mirror of
				https://gitee.com/gitea/gitea
				synced 2025-11-04 16:40:24 +08:00 
			
		
		
		
	Refactor: Move login out of models (#16199)
`models` does far too much. In particular it handles all `UserSignin`. It shouldn't be responsible for calling LDAP, SMTP or PAM for signing in. Therefore we should move this code out of `models`. This code has to depend on `models` - therefore it belongs in `services`. There is a package in `services` called `auth` and clearly this functionality belongs in there. Plan: - [x] Change `auth.Auth` to `auth.Method` - as they represent methods of authentication. - [x] Move `models.UserSignIn` into `auth` - [x] Move `models.ExternalUserLogin` - [x] Move most of the `LoginVia*` methods to `auth` or subpackages - [x] Move Resynchronize functionality to `auth` - Involved some restructuring of `models/ssh_key.go` to reduce the size of this massive file and simplify its files. - [x] Move the rest of the LDAP functionality in to the ldap subpackage - [x] Re-factor the login sources to express an interfaces `auth.Source`? - I've done this through some smaller interfaces Authenticator and Synchronizable - which would allow us to extend things in future - [x] Now LDAP is out of models - need to think about modules/auth/ldap and I think all of that functionality might just be moveable - [x] Similarly a lot Oauth2 functionality need not be in models too and should be moved to services/auth/source/oauth2 - [x] modules/auth/oauth2/oauth2.go uses xorm... This is naughty - probably need to move this into models. - [x] models/oauth2.go - mostly should be in modules/auth/oauth2 or services/auth/source/oauth2 - [x] More simplifications of login_source.go may need to be done - Allow wiring in of notify registration - *this can now easily be done - but I think we should do it in another PR* - see #16178 - More refactors...? - OpenID should probably become an auth Method but I think that can be left for another PR - Methods should also probably be cleaned up - again another PR I think. - SSPI still needs more refactors.* Rename auth.Auth auth.Method * Restructure ssh_key.go - move functions from models/user.go that relate to ssh_key to ssh_key - split ssh_key.go to try create clearer function domains for allow for future refactors here. Signed-off-by: Andrew Thornton <art27@cantab.net>
This commit is contained in:
		
							
								
								
									
										12
									
								
								cmd/admin.go
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								cmd/admin.go
									
									
									
									
									
								
							@@ -14,7 +14,6 @@ import (
 | 
				
			|||||||
	"text/tabwriter"
 | 
						"text/tabwriter"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"code.gitea.io/gitea/models"
 | 
						"code.gitea.io/gitea/models"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/auth/oauth2"
 | 
					 | 
				
			||||||
	"code.gitea.io/gitea/modules/git"
 | 
						"code.gitea.io/gitea/modules/git"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/graceful"
 | 
						"code.gitea.io/gitea/modules/graceful"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/log"
 | 
						"code.gitea.io/gitea/modules/log"
 | 
				
			||||||
@@ -22,6 +21,7 @@ import (
 | 
				
			|||||||
	repo_module "code.gitea.io/gitea/modules/repository"
 | 
						repo_module "code.gitea.io/gitea/modules/repository"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/setting"
 | 
						"code.gitea.io/gitea/modules/setting"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/storage"
 | 
						"code.gitea.io/gitea/modules/storage"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/services/auth/source/oauth2"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"github.com/urfave/cli"
 | 
						"github.com/urfave/cli"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
@@ -597,7 +597,7 @@ func runRegenerateKeys(_ *cli.Context) error {
 | 
				
			|||||||
	return models.RewriteAllPublicKeys()
 | 
						return models.RewriteAllPublicKeys()
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func parseOAuth2Config(c *cli.Context) *models.OAuth2Config {
 | 
					func parseOAuth2Config(c *cli.Context) *oauth2.Source {
 | 
				
			||||||
	var customURLMapping *oauth2.CustomURLMapping
 | 
						var customURLMapping *oauth2.CustomURLMapping
 | 
				
			||||||
	if c.IsSet("use-custom-urls") {
 | 
						if c.IsSet("use-custom-urls") {
 | 
				
			||||||
		customURLMapping = &oauth2.CustomURLMapping{
 | 
							customURLMapping = &oauth2.CustomURLMapping{
 | 
				
			||||||
@@ -609,7 +609,7 @@ func parseOAuth2Config(c *cli.Context) *models.OAuth2Config {
 | 
				
			|||||||
	} else {
 | 
						} else {
 | 
				
			||||||
		customURLMapping = nil
 | 
							customURLMapping = nil
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	return &models.OAuth2Config{
 | 
						return &oauth2.Source{
 | 
				
			||||||
		Provider:                      c.String("provider"),
 | 
							Provider:                      c.String("provider"),
 | 
				
			||||||
		ClientID:                      c.String("key"),
 | 
							ClientID:                      c.String("key"),
 | 
				
			||||||
		ClientSecret:                  c.String("secret"),
 | 
							ClientSecret:                  c.String("secret"),
 | 
				
			||||||
@@ -627,7 +627,7 @@ func runAddOauth(c *cli.Context) error {
 | 
				
			|||||||
	return models.CreateLoginSource(&models.LoginSource{
 | 
						return models.CreateLoginSource(&models.LoginSource{
 | 
				
			||||||
		Type:     models.LoginOAuth2,
 | 
							Type:     models.LoginOAuth2,
 | 
				
			||||||
		Name:     c.String("name"),
 | 
							Name:     c.String("name"),
 | 
				
			||||||
		IsActived: true,
 | 
							IsActive: true,
 | 
				
			||||||
		Cfg:      parseOAuth2Config(c),
 | 
							Cfg:      parseOAuth2Config(c),
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -646,7 +646,7 @@ func runUpdateOauth(c *cli.Context) error {
 | 
				
			|||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	oAuth2Config := source.OAuth2()
 | 
						oAuth2Config := source.Cfg.(*oauth2.Source)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if c.IsSet("name") {
 | 
						if c.IsSet("name") {
 | 
				
			||||||
		source.Name = c.String("name")
 | 
							source.Name = c.String("name")
 | 
				
			||||||
@@ -728,7 +728,7 @@ func runListAuth(c *cli.Context) error {
 | 
				
			|||||||
	w := tabwriter.NewWriter(os.Stdout, c.Int("min-width"), c.Int("tab-width"), c.Int("padding"), padChar, flags)
 | 
						w := tabwriter.NewWriter(os.Stdout, c.Int("min-width"), c.Int("tab-width"), c.Int("padding"), padChar, flags)
 | 
				
			||||||
	fmt.Fprintf(w, "ID\tName\tType\tEnabled\n")
 | 
						fmt.Fprintf(w, "ID\tName\tType\tEnabled\n")
 | 
				
			||||||
	for _, source := range loginSources {
 | 
						for _, source := range loginSources {
 | 
				
			||||||
		fmt.Fprintf(w, "%d\t%s\t%s\t%t\n", source.ID, source.Name, models.LoginNames[source.Type], source.IsActived)
 | 
							fmt.Fprintf(w, "%d\t%s\t%s\t%t\n", source.ID, source.Name, models.LoginNames[source.Type], source.IsActive)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	w.Flush()
 | 
						w.Flush()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,7 +9,7 @@ import (
 | 
				
			|||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"code.gitea.io/gitea/models"
 | 
						"code.gitea.io/gitea/models"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/auth/ldap"
 | 
						"code.gitea.io/gitea/services/auth/source/ldap"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"github.com/urfave/cli"
 | 
						"github.com/urfave/cli"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
@@ -172,7 +172,7 @@ func parseLoginSource(c *cli.Context, loginSource *models.LoginSource) {
 | 
				
			|||||||
		loginSource.Name = c.String("name")
 | 
							loginSource.Name = c.String("name")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if c.IsSet("not-active") {
 | 
						if c.IsSet("not-active") {
 | 
				
			||||||
		loginSource.IsActived = !c.Bool("not-active")
 | 
							loginSource.IsActive = !c.Bool("not-active")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if c.IsSet("synchronize-users") {
 | 
						if c.IsSet("synchronize-users") {
 | 
				
			||||||
		loginSource.IsSyncEnabled = c.Bool("synchronize-users")
 | 
							loginSource.IsSyncEnabled = c.Bool("synchronize-users")
 | 
				
			||||||
@@ -180,70 +180,70 @@ func parseLoginSource(c *cli.Context, loginSource *models.LoginSource) {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// parseLdapConfig assigns values on config according to command line flags.
 | 
					// parseLdapConfig assigns values on config according to command line flags.
 | 
				
			||||||
func parseLdapConfig(c *cli.Context, config *models.LDAPConfig) error {
 | 
					func parseLdapConfig(c *cli.Context, config *ldap.Source) error {
 | 
				
			||||||
	if c.IsSet("name") {
 | 
						if c.IsSet("name") {
 | 
				
			||||||
		config.Source.Name = c.String("name")
 | 
							config.Name = c.String("name")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if c.IsSet("host") {
 | 
						if c.IsSet("host") {
 | 
				
			||||||
		config.Source.Host = c.String("host")
 | 
							config.Host = c.String("host")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if c.IsSet("port") {
 | 
						if c.IsSet("port") {
 | 
				
			||||||
		config.Source.Port = c.Int("port")
 | 
							config.Port = c.Int("port")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if c.IsSet("security-protocol") {
 | 
						if c.IsSet("security-protocol") {
 | 
				
			||||||
		p, ok := findLdapSecurityProtocolByName(c.String("security-protocol"))
 | 
							p, ok := findLdapSecurityProtocolByName(c.String("security-protocol"))
 | 
				
			||||||
		if !ok {
 | 
							if !ok {
 | 
				
			||||||
			return fmt.Errorf("Unknown security protocol name: %s", c.String("security-protocol"))
 | 
								return fmt.Errorf("Unknown security protocol name: %s", c.String("security-protocol"))
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		config.Source.SecurityProtocol = p
 | 
							config.SecurityProtocol = p
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if c.IsSet("skip-tls-verify") {
 | 
						if c.IsSet("skip-tls-verify") {
 | 
				
			||||||
		config.Source.SkipVerify = c.Bool("skip-tls-verify")
 | 
							config.SkipVerify = c.Bool("skip-tls-verify")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if c.IsSet("bind-dn") {
 | 
						if c.IsSet("bind-dn") {
 | 
				
			||||||
		config.Source.BindDN = c.String("bind-dn")
 | 
							config.BindDN = c.String("bind-dn")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if c.IsSet("user-dn") {
 | 
						if c.IsSet("user-dn") {
 | 
				
			||||||
		config.Source.UserDN = c.String("user-dn")
 | 
							config.UserDN = c.String("user-dn")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if c.IsSet("bind-password") {
 | 
						if c.IsSet("bind-password") {
 | 
				
			||||||
		config.Source.BindPassword = c.String("bind-password")
 | 
							config.BindPassword = c.String("bind-password")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if c.IsSet("user-search-base") {
 | 
						if c.IsSet("user-search-base") {
 | 
				
			||||||
		config.Source.UserBase = c.String("user-search-base")
 | 
							config.UserBase = c.String("user-search-base")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if c.IsSet("username-attribute") {
 | 
						if c.IsSet("username-attribute") {
 | 
				
			||||||
		config.Source.AttributeUsername = c.String("username-attribute")
 | 
							config.AttributeUsername = c.String("username-attribute")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if c.IsSet("firstname-attribute") {
 | 
						if c.IsSet("firstname-attribute") {
 | 
				
			||||||
		config.Source.AttributeName = c.String("firstname-attribute")
 | 
							config.AttributeName = c.String("firstname-attribute")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if c.IsSet("surname-attribute") {
 | 
						if c.IsSet("surname-attribute") {
 | 
				
			||||||
		config.Source.AttributeSurname = c.String("surname-attribute")
 | 
							config.AttributeSurname = c.String("surname-attribute")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if c.IsSet("email-attribute") {
 | 
						if c.IsSet("email-attribute") {
 | 
				
			||||||
		config.Source.AttributeMail = c.String("email-attribute")
 | 
							config.AttributeMail = c.String("email-attribute")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if c.IsSet("attributes-in-bind") {
 | 
						if c.IsSet("attributes-in-bind") {
 | 
				
			||||||
		config.Source.AttributesInBind = c.Bool("attributes-in-bind")
 | 
							config.AttributesInBind = c.Bool("attributes-in-bind")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if c.IsSet("public-ssh-key-attribute") {
 | 
						if c.IsSet("public-ssh-key-attribute") {
 | 
				
			||||||
		config.Source.AttributeSSHPublicKey = c.String("public-ssh-key-attribute")
 | 
							config.AttributeSSHPublicKey = c.String("public-ssh-key-attribute")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if c.IsSet("page-size") {
 | 
						if c.IsSet("page-size") {
 | 
				
			||||||
		config.Source.SearchPageSize = uint32(c.Uint("page-size"))
 | 
							config.SearchPageSize = uint32(c.Uint("page-size"))
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if c.IsSet("user-filter") {
 | 
						if c.IsSet("user-filter") {
 | 
				
			||||||
		config.Source.Filter = c.String("user-filter")
 | 
							config.Filter = c.String("user-filter")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if c.IsSet("admin-filter") {
 | 
						if c.IsSet("admin-filter") {
 | 
				
			||||||
		config.Source.AdminFilter = c.String("admin-filter")
 | 
							config.AdminFilter = c.String("admin-filter")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if c.IsSet("restricted-filter") {
 | 
						if c.IsSet("restricted-filter") {
 | 
				
			||||||
		config.Source.RestrictedFilter = c.String("restricted-filter")
 | 
							config.RestrictedFilter = c.String("restricted-filter")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if c.IsSet("allow-deactivate-all") {
 | 
						if c.IsSet("allow-deactivate-all") {
 | 
				
			||||||
		config.Source.AllowDeactivateAll = c.Bool("allow-deactivate-all")
 | 
							config.AllowDeactivateAll = c.Bool("allow-deactivate-all")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	return nil
 | 
						return nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -251,7 +251,7 @@ func parseLdapConfig(c *cli.Context, config *models.LDAPConfig) error {
 | 
				
			|||||||
// findLdapSecurityProtocolByName finds security protocol by its name ignoring case.
 | 
					// findLdapSecurityProtocolByName finds security protocol by its name ignoring case.
 | 
				
			||||||
// It returns the value of the security protocol and if it was found.
 | 
					// It returns the value of the security protocol and if it was found.
 | 
				
			||||||
func findLdapSecurityProtocolByName(name string) (ldap.SecurityProtocol, bool) {
 | 
					func findLdapSecurityProtocolByName(name string) (ldap.SecurityProtocol, bool) {
 | 
				
			||||||
	for i, n := range models.SecurityProtocolNames {
 | 
						for i, n := range ldap.SecurityProtocolNames {
 | 
				
			||||||
		if strings.EqualFold(name, n) {
 | 
							if strings.EqualFold(name, n) {
 | 
				
			||||||
			return i, true
 | 
								return i, true
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@@ -290,16 +290,14 @@ func (a *authService) addLdapBindDn(c *cli.Context) error {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	loginSource := &models.LoginSource{
 | 
						loginSource := &models.LoginSource{
 | 
				
			||||||
		Type:     models.LoginLDAP,
 | 
							Type:     models.LoginLDAP,
 | 
				
			||||||
		IsActived: true, // active by default
 | 
							IsActive: true, // active by default
 | 
				
			||||||
		Cfg: &models.LDAPConfig{
 | 
							Cfg: &ldap.Source{
 | 
				
			||||||
			Source: &ldap.Source{
 | 
					 | 
				
			||||||
			Enabled: true, // always true
 | 
								Enabled: true, // always true
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	parseLoginSource(c, loginSource)
 | 
						parseLoginSource(c, loginSource)
 | 
				
			||||||
	if err := parseLdapConfig(c, loginSource.LDAP()); err != nil {
 | 
						if err := parseLdapConfig(c, loginSource.Cfg.(*ldap.Source)); err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -318,7 +316,7 @@ func (a *authService) updateLdapBindDn(c *cli.Context) error {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	parseLoginSource(c, loginSource)
 | 
						parseLoginSource(c, loginSource)
 | 
				
			||||||
	if err := parseLdapConfig(c, loginSource.LDAP()); err != nil {
 | 
						if err := parseLdapConfig(c, loginSource.Cfg.(*ldap.Source)); err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -337,16 +335,14 @@ func (a *authService) addLdapSimpleAuth(c *cli.Context) error {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	loginSource := &models.LoginSource{
 | 
						loginSource := &models.LoginSource{
 | 
				
			||||||
		Type:     models.LoginDLDAP,
 | 
							Type:     models.LoginDLDAP,
 | 
				
			||||||
		IsActived: true, // active by default
 | 
							IsActive: true, // active by default
 | 
				
			||||||
		Cfg: &models.LDAPConfig{
 | 
							Cfg: &ldap.Source{
 | 
				
			||||||
			Source: &ldap.Source{
 | 
					 | 
				
			||||||
			Enabled: true, // always true
 | 
								Enabled: true, // always true
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	parseLoginSource(c, loginSource)
 | 
						parseLoginSource(c, loginSource)
 | 
				
			||||||
	if err := parseLdapConfig(c, loginSource.LDAP()); err != nil {
 | 
						if err := parseLdapConfig(c, loginSource.Cfg.(*ldap.Source)); err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -365,7 +361,7 @@ func (a *authService) updateLdapSimpleAuth(c *cli.Context) error {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	parseLoginSource(c, loginSource)
 | 
						parseLoginSource(c, loginSource)
 | 
				
			||||||
	if err := parseLdapConfig(c, loginSource.LDAP()); err != nil {
 | 
						if err := parseLdapConfig(c, loginSource.Cfg.(*ldap.Source)); err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,7 +8,7 @@ import (
 | 
				
			|||||||
	"testing"
 | 
						"testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"code.gitea.io/gitea/models"
 | 
						"code.gitea.io/gitea/models"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/auth/ldap"
 | 
						"code.gitea.io/gitea/services/auth/source/ldap"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"github.com/stretchr/testify/assert"
 | 
						"github.com/stretchr/testify/assert"
 | 
				
			||||||
	"github.com/urfave/cli"
 | 
						"github.com/urfave/cli"
 | 
				
			||||||
@@ -54,10 +54,9 @@ func TestAddLdapBindDn(t *testing.T) {
 | 
				
			|||||||
			loginSource: &models.LoginSource{
 | 
								loginSource: &models.LoginSource{
 | 
				
			||||||
				Type:          models.LoginLDAP,
 | 
									Type:          models.LoginLDAP,
 | 
				
			||||||
				Name:          "ldap (via Bind DN) source full",
 | 
									Name:          "ldap (via Bind DN) source full",
 | 
				
			||||||
				IsActived:     false,
 | 
									IsActive:      false,
 | 
				
			||||||
				IsSyncEnabled: true,
 | 
									IsSyncEnabled: true,
 | 
				
			||||||
				Cfg: &models.LDAPConfig{
 | 
									Cfg: &ldap.Source{
 | 
				
			||||||
					Source: &ldap.Source{
 | 
					 | 
				
			||||||
					Name:                  "ldap (via Bind DN) source full",
 | 
										Name:                  "ldap (via Bind DN) source full",
 | 
				
			||||||
					Host:                  "ldap-bind-server full",
 | 
										Host:                  "ldap-bind-server full",
 | 
				
			||||||
					Port:                  9876,
 | 
										Port:                  9876,
 | 
				
			||||||
@@ -80,7 +79,6 @@ func TestAddLdapBindDn(t *testing.T) {
 | 
				
			|||||||
				},
 | 
									},
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		// case 1
 | 
							// case 1
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			args: []string{
 | 
								args: []string{
 | 
				
			||||||
@@ -96,9 +94,8 @@ func TestAddLdapBindDn(t *testing.T) {
 | 
				
			|||||||
			loginSource: &models.LoginSource{
 | 
								loginSource: &models.LoginSource{
 | 
				
			||||||
				Type:     models.LoginLDAP,
 | 
									Type:     models.LoginLDAP,
 | 
				
			||||||
				Name:     "ldap (via Bind DN) source min",
 | 
									Name:     "ldap (via Bind DN) source min",
 | 
				
			||||||
				IsActived: true,
 | 
									IsActive: true,
 | 
				
			||||||
				Cfg: &models.LDAPConfig{
 | 
									Cfg: &ldap.Source{
 | 
				
			||||||
					Source: &ldap.Source{
 | 
					 | 
				
			||||||
					Name:             "ldap (via Bind DN) source min",
 | 
										Name:             "ldap (via Bind DN) source min",
 | 
				
			||||||
					Host:             "ldap-bind-server min",
 | 
										Host:             "ldap-bind-server min",
 | 
				
			||||||
					Port:             1234,
 | 
										Port:             1234,
 | 
				
			||||||
@@ -110,7 +107,6 @@ func TestAddLdapBindDn(t *testing.T) {
 | 
				
			|||||||
				},
 | 
									},
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		// case 2
 | 
							// case 2
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			args: []string{
 | 
								args: []string{
 | 
				
			||||||
@@ -278,9 +274,8 @@ func TestAddLdapSimpleAuth(t *testing.T) {
 | 
				
			|||||||
			loginSource: &models.LoginSource{
 | 
								loginSource: &models.LoginSource{
 | 
				
			||||||
				Type:     models.LoginDLDAP,
 | 
									Type:     models.LoginDLDAP,
 | 
				
			||||||
				Name:     "ldap (simple auth) source full",
 | 
									Name:     "ldap (simple auth) source full",
 | 
				
			||||||
				IsActived: false,
 | 
									IsActive: false,
 | 
				
			||||||
				Cfg: &models.LDAPConfig{
 | 
									Cfg: &ldap.Source{
 | 
				
			||||||
					Source: &ldap.Source{
 | 
					 | 
				
			||||||
					Name:                  "ldap (simple auth) source full",
 | 
										Name:                  "ldap (simple auth) source full",
 | 
				
			||||||
					Host:                  "ldap-simple-server full",
 | 
										Host:                  "ldap-simple-server full",
 | 
				
			||||||
					Port:                  987,
 | 
										Port:                  987,
 | 
				
			||||||
@@ -300,7 +295,6 @@ func TestAddLdapSimpleAuth(t *testing.T) {
 | 
				
			|||||||
				},
 | 
									},
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		// case 1
 | 
							// case 1
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			args: []string{
 | 
								args: []string{
 | 
				
			||||||
@@ -316,9 +310,8 @@ func TestAddLdapSimpleAuth(t *testing.T) {
 | 
				
			|||||||
			loginSource: &models.LoginSource{
 | 
								loginSource: &models.LoginSource{
 | 
				
			||||||
				Type:     models.LoginDLDAP,
 | 
									Type:     models.LoginDLDAP,
 | 
				
			||||||
				Name:     "ldap (simple auth) source min",
 | 
									Name:     "ldap (simple auth) source min",
 | 
				
			||||||
				IsActived: true,
 | 
									IsActive: true,
 | 
				
			||||||
				Cfg: &models.LDAPConfig{
 | 
									Cfg: &ldap.Source{
 | 
				
			||||||
					Source: &ldap.Source{
 | 
					 | 
				
			||||||
					Name:             "ldap (simple auth) source min",
 | 
										Name:             "ldap (simple auth) source min",
 | 
				
			||||||
					Host:             "ldap-simple-server min",
 | 
										Host:             "ldap-simple-server min",
 | 
				
			||||||
					Port:             123,
 | 
										Port:             123,
 | 
				
			||||||
@@ -330,7 +323,6 @@ func TestAddLdapSimpleAuth(t *testing.T) {
 | 
				
			|||||||
				},
 | 
									},
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		// case 2
 | 
							// case 2
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			args: []string{
 | 
								args: []string{
 | 
				
			||||||
@@ -517,20 +509,17 @@ func TestUpdateLdapBindDn(t *testing.T) {
 | 
				
			|||||||
			id: 23,
 | 
								id: 23,
 | 
				
			||||||
			existingLoginSource: &models.LoginSource{
 | 
								existingLoginSource: &models.LoginSource{
 | 
				
			||||||
				Type:     models.LoginLDAP,
 | 
									Type:     models.LoginLDAP,
 | 
				
			||||||
				IsActived: true,
 | 
									IsActive: true,
 | 
				
			||||||
				Cfg: &models.LDAPConfig{
 | 
									Cfg: &ldap.Source{
 | 
				
			||||||
					Source: &ldap.Source{
 | 
					 | 
				
			||||||
					Enabled: true,
 | 
										Enabled: true,
 | 
				
			||||||
				},
 | 
									},
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
			},
 | 
					 | 
				
			||||||
			loginSource: &models.LoginSource{
 | 
								loginSource: &models.LoginSource{
 | 
				
			||||||
				Type:          models.LoginLDAP,
 | 
									Type:          models.LoginLDAP,
 | 
				
			||||||
				Name:          "ldap (via Bind DN) source full",
 | 
									Name:          "ldap (via Bind DN) source full",
 | 
				
			||||||
				IsActived:     false,
 | 
									IsActive:      false,
 | 
				
			||||||
				IsSyncEnabled: true,
 | 
									IsSyncEnabled: true,
 | 
				
			||||||
				Cfg: &models.LDAPConfig{
 | 
									Cfg: &ldap.Source{
 | 
				
			||||||
					Source: &ldap.Source{
 | 
					 | 
				
			||||||
					Name:                  "ldap (via Bind DN) source full",
 | 
										Name:                  "ldap (via Bind DN) source full",
 | 
				
			||||||
					Host:                  "ldap-bind-server full",
 | 
										Host:                  "ldap-bind-server full",
 | 
				
			||||||
					Port:                  9876,
 | 
										Port:                  9876,
 | 
				
			||||||
@@ -553,7 +542,6 @@ func TestUpdateLdapBindDn(t *testing.T) {
 | 
				
			|||||||
				},
 | 
									},
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		// case 1
 | 
							// case 1
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			args: []string{
 | 
								args: []string{
 | 
				
			||||||
@@ -562,9 +550,7 @@ func TestUpdateLdapBindDn(t *testing.T) {
 | 
				
			|||||||
			},
 | 
								},
 | 
				
			||||||
			loginSource: &models.LoginSource{
 | 
								loginSource: &models.LoginSource{
 | 
				
			||||||
				Type: models.LoginLDAP,
 | 
									Type: models.LoginLDAP,
 | 
				
			||||||
				Cfg: &models.LDAPConfig{
 | 
									Cfg:  &ldap.Source{},
 | 
				
			||||||
					Source: &ldap.Source{},
 | 
					 | 
				
			||||||
				},
 | 
					 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		// case 2
 | 
							// case 2
 | 
				
			||||||
@@ -577,13 +563,11 @@ func TestUpdateLdapBindDn(t *testing.T) {
 | 
				
			|||||||
			loginSource: &models.LoginSource{
 | 
								loginSource: &models.LoginSource{
 | 
				
			||||||
				Type: models.LoginLDAP,
 | 
									Type: models.LoginLDAP,
 | 
				
			||||||
				Name: "ldap (via Bind DN) source",
 | 
									Name: "ldap (via Bind DN) source",
 | 
				
			||||||
				Cfg: &models.LDAPConfig{
 | 
									Cfg: &ldap.Source{
 | 
				
			||||||
					Source: &ldap.Source{
 | 
					 | 
				
			||||||
					Name: "ldap (via Bind DN) source",
 | 
										Name: "ldap (via Bind DN) source",
 | 
				
			||||||
				},
 | 
									},
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		// case 3
 | 
							// case 3
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			args: []string{
 | 
								args: []string{
 | 
				
			||||||
@@ -593,17 +577,13 @@ func TestUpdateLdapBindDn(t *testing.T) {
 | 
				
			|||||||
			},
 | 
								},
 | 
				
			||||||
			existingLoginSource: &models.LoginSource{
 | 
								existingLoginSource: &models.LoginSource{
 | 
				
			||||||
				Type:     models.LoginLDAP,
 | 
									Type:     models.LoginLDAP,
 | 
				
			||||||
				IsActived: true,
 | 
									IsActive: true,
 | 
				
			||||||
				Cfg: &models.LDAPConfig{
 | 
									Cfg:      &ldap.Source{},
 | 
				
			||||||
					Source: &ldap.Source{},
 | 
					 | 
				
			||||||
				},
 | 
					 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
			loginSource: &models.LoginSource{
 | 
								loginSource: &models.LoginSource{
 | 
				
			||||||
				Type:     models.LoginLDAP,
 | 
									Type:     models.LoginLDAP,
 | 
				
			||||||
				IsActived: false,
 | 
									IsActive: false,
 | 
				
			||||||
				Cfg: &models.LDAPConfig{
 | 
									Cfg:      &ldap.Source{},
 | 
				
			||||||
					Source: &ldap.Source{},
 | 
					 | 
				
			||||||
				},
 | 
					 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		// case 4
 | 
							// case 4
 | 
				
			||||||
@@ -615,13 +595,11 @@ func TestUpdateLdapBindDn(t *testing.T) {
 | 
				
			|||||||
			},
 | 
								},
 | 
				
			||||||
			loginSource: &models.LoginSource{
 | 
								loginSource: &models.LoginSource{
 | 
				
			||||||
				Type: models.LoginLDAP,
 | 
									Type: models.LoginLDAP,
 | 
				
			||||||
				Cfg: &models.LDAPConfig{
 | 
									Cfg: &ldap.Source{
 | 
				
			||||||
					Source: &ldap.Source{
 | 
					 | 
				
			||||||
					SecurityProtocol: ldap.SecurityProtocol(1),
 | 
										SecurityProtocol: ldap.SecurityProtocol(1),
 | 
				
			||||||
				},
 | 
									},
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		// case 5
 | 
							// case 5
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			args: []string{
 | 
								args: []string{
 | 
				
			||||||
@@ -631,13 +609,11 @@ func TestUpdateLdapBindDn(t *testing.T) {
 | 
				
			|||||||
			},
 | 
								},
 | 
				
			||||||
			loginSource: &models.LoginSource{
 | 
								loginSource: &models.LoginSource{
 | 
				
			||||||
				Type: models.LoginLDAP,
 | 
									Type: models.LoginLDAP,
 | 
				
			||||||
				Cfg: &models.LDAPConfig{
 | 
									Cfg: &ldap.Source{
 | 
				
			||||||
					Source: &ldap.Source{
 | 
					 | 
				
			||||||
					SkipVerify: true,
 | 
										SkipVerify: true,
 | 
				
			||||||
				},
 | 
									},
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		// case 6
 | 
							// case 6
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			args: []string{
 | 
								args: []string{
 | 
				
			||||||
@@ -647,13 +623,11 @@ func TestUpdateLdapBindDn(t *testing.T) {
 | 
				
			|||||||
			},
 | 
								},
 | 
				
			||||||
			loginSource: &models.LoginSource{
 | 
								loginSource: &models.LoginSource{
 | 
				
			||||||
				Type: models.LoginLDAP,
 | 
									Type: models.LoginLDAP,
 | 
				
			||||||
				Cfg: &models.LDAPConfig{
 | 
									Cfg: &ldap.Source{
 | 
				
			||||||
					Source: &ldap.Source{
 | 
					 | 
				
			||||||
					Host: "ldap-server",
 | 
										Host: "ldap-server",
 | 
				
			||||||
				},
 | 
									},
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		// case 7
 | 
							// case 7
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			args: []string{
 | 
								args: []string{
 | 
				
			||||||
@@ -663,13 +637,11 @@ func TestUpdateLdapBindDn(t *testing.T) {
 | 
				
			|||||||
			},
 | 
								},
 | 
				
			||||||
			loginSource: &models.LoginSource{
 | 
								loginSource: &models.LoginSource{
 | 
				
			||||||
				Type: models.LoginLDAP,
 | 
									Type: models.LoginLDAP,
 | 
				
			||||||
				Cfg: &models.LDAPConfig{
 | 
									Cfg: &ldap.Source{
 | 
				
			||||||
					Source: &ldap.Source{
 | 
					 | 
				
			||||||
					Port: 389,
 | 
										Port: 389,
 | 
				
			||||||
				},
 | 
									},
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		// case 8
 | 
							// case 8
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			args: []string{
 | 
								args: []string{
 | 
				
			||||||
@@ -679,13 +651,11 @@ func TestUpdateLdapBindDn(t *testing.T) {
 | 
				
			|||||||
			},
 | 
								},
 | 
				
			||||||
			loginSource: &models.LoginSource{
 | 
								loginSource: &models.LoginSource{
 | 
				
			||||||
				Type: models.LoginLDAP,
 | 
									Type: models.LoginLDAP,
 | 
				
			||||||
				Cfg: &models.LDAPConfig{
 | 
									Cfg: &ldap.Source{
 | 
				
			||||||
					Source: &ldap.Source{
 | 
					 | 
				
			||||||
					UserBase: "ou=Users,dc=domain,dc=org",
 | 
										UserBase: "ou=Users,dc=domain,dc=org",
 | 
				
			||||||
				},
 | 
									},
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		// case 9
 | 
							// case 9
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			args: []string{
 | 
								args: []string{
 | 
				
			||||||
@@ -695,13 +665,11 @@ func TestUpdateLdapBindDn(t *testing.T) {
 | 
				
			|||||||
			},
 | 
								},
 | 
				
			||||||
			loginSource: &models.LoginSource{
 | 
								loginSource: &models.LoginSource{
 | 
				
			||||||
				Type: models.LoginLDAP,
 | 
									Type: models.LoginLDAP,
 | 
				
			||||||
				Cfg: &models.LDAPConfig{
 | 
									Cfg: &ldap.Source{
 | 
				
			||||||
					Source: &ldap.Source{
 | 
					 | 
				
			||||||
					Filter: "(memberOf=cn=user-group,ou=example,dc=domain,dc=org)",
 | 
										Filter: "(memberOf=cn=user-group,ou=example,dc=domain,dc=org)",
 | 
				
			||||||
				},
 | 
									},
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		// case 10
 | 
							// case 10
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			args: []string{
 | 
								args: []string{
 | 
				
			||||||
@@ -711,13 +679,11 @@ func TestUpdateLdapBindDn(t *testing.T) {
 | 
				
			|||||||
			},
 | 
								},
 | 
				
			||||||
			loginSource: &models.LoginSource{
 | 
								loginSource: &models.LoginSource{
 | 
				
			||||||
				Type: models.LoginLDAP,
 | 
									Type: models.LoginLDAP,
 | 
				
			||||||
				Cfg: &models.LDAPConfig{
 | 
									Cfg: &ldap.Source{
 | 
				
			||||||
					Source: &ldap.Source{
 | 
					 | 
				
			||||||
					AdminFilter: "(memberOf=cn=admin-group,ou=example,dc=domain,dc=org)",
 | 
										AdminFilter: "(memberOf=cn=admin-group,ou=example,dc=domain,dc=org)",
 | 
				
			||||||
				},
 | 
									},
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		// case 11
 | 
							// case 11
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			args: []string{
 | 
								args: []string{
 | 
				
			||||||
@@ -727,13 +693,11 @@ func TestUpdateLdapBindDn(t *testing.T) {
 | 
				
			|||||||
			},
 | 
								},
 | 
				
			||||||
			loginSource: &models.LoginSource{
 | 
								loginSource: &models.LoginSource{
 | 
				
			||||||
				Type: models.LoginLDAP,
 | 
									Type: models.LoginLDAP,
 | 
				
			||||||
				Cfg: &models.LDAPConfig{
 | 
									Cfg: &ldap.Source{
 | 
				
			||||||
					Source: &ldap.Source{
 | 
					 | 
				
			||||||
					AttributeUsername: "uid",
 | 
										AttributeUsername: "uid",
 | 
				
			||||||
				},
 | 
									},
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		// case 12
 | 
							// case 12
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			args: []string{
 | 
								args: []string{
 | 
				
			||||||
@@ -743,13 +707,11 @@ func TestUpdateLdapBindDn(t *testing.T) {
 | 
				
			|||||||
			},
 | 
								},
 | 
				
			||||||
			loginSource: &models.LoginSource{
 | 
								loginSource: &models.LoginSource{
 | 
				
			||||||
				Type: models.LoginLDAP,
 | 
									Type: models.LoginLDAP,
 | 
				
			||||||
				Cfg: &models.LDAPConfig{
 | 
									Cfg: &ldap.Source{
 | 
				
			||||||
					Source: &ldap.Source{
 | 
					 | 
				
			||||||
					AttributeName: "givenName",
 | 
										AttributeName: "givenName",
 | 
				
			||||||
				},
 | 
									},
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		// case 13
 | 
							// case 13
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			args: []string{
 | 
								args: []string{
 | 
				
			||||||
@@ -759,13 +721,11 @@ func TestUpdateLdapBindDn(t *testing.T) {
 | 
				
			|||||||
			},
 | 
								},
 | 
				
			||||||
			loginSource: &models.LoginSource{
 | 
								loginSource: &models.LoginSource{
 | 
				
			||||||
				Type: models.LoginLDAP,
 | 
									Type: models.LoginLDAP,
 | 
				
			||||||
				Cfg: &models.LDAPConfig{
 | 
									Cfg: &ldap.Source{
 | 
				
			||||||
					Source: &ldap.Source{
 | 
					 | 
				
			||||||
					AttributeSurname: "sn",
 | 
										AttributeSurname: "sn",
 | 
				
			||||||
				},
 | 
									},
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		// case 14
 | 
							// case 14
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			args: []string{
 | 
								args: []string{
 | 
				
			||||||
@@ -775,13 +735,11 @@ func TestUpdateLdapBindDn(t *testing.T) {
 | 
				
			|||||||
			},
 | 
								},
 | 
				
			||||||
			loginSource: &models.LoginSource{
 | 
								loginSource: &models.LoginSource{
 | 
				
			||||||
				Type: models.LoginLDAP,
 | 
									Type: models.LoginLDAP,
 | 
				
			||||||
				Cfg: &models.LDAPConfig{
 | 
									Cfg: &ldap.Source{
 | 
				
			||||||
					Source: &ldap.Source{
 | 
					 | 
				
			||||||
					AttributeMail: "mail",
 | 
										AttributeMail: "mail",
 | 
				
			||||||
				},
 | 
									},
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		// case 15
 | 
							// case 15
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			args: []string{
 | 
								args: []string{
 | 
				
			||||||
@@ -791,13 +749,11 @@ func TestUpdateLdapBindDn(t *testing.T) {
 | 
				
			|||||||
			},
 | 
								},
 | 
				
			||||||
			loginSource: &models.LoginSource{
 | 
								loginSource: &models.LoginSource{
 | 
				
			||||||
				Type: models.LoginLDAP,
 | 
									Type: models.LoginLDAP,
 | 
				
			||||||
				Cfg: &models.LDAPConfig{
 | 
									Cfg: &ldap.Source{
 | 
				
			||||||
					Source: &ldap.Source{
 | 
					 | 
				
			||||||
					AttributesInBind: true,
 | 
										AttributesInBind: true,
 | 
				
			||||||
				},
 | 
									},
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		// case 16
 | 
							// case 16
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			args: []string{
 | 
								args: []string{
 | 
				
			||||||
@@ -807,13 +763,11 @@ func TestUpdateLdapBindDn(t *testing.T) {
 | 
				
			|||||||
			},
 | 
								},
 | 
				
			||||||
			loginSource: &models.LoginSource{
 | 
								loginSource: &models.LoginSource{
 | 
				
			||||||
				Type: models.LoginLDAP,
 | 
									Type: models.LoginLDAP,
 | 
				
			||||||
				Cfg: &models.LDAPConfig{
 | 
									Cfg: &ldap.Source{
 | 
				
			||||||
					Source: &ldap.Source{
 | 
					 | 
				
			||||||
					AttributeSSHPublicKey: "publickey",
 | 
										AttributeSSHPublicKey: "publickey",
 | 
				
			||||||
				},
 | 
									},
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		// case 17
 | 
							// case 17
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			args: []string{
 | 
								args: []string{
 | 
				
			||||||
@@ -823,13 +777,11 @@ func TestUpdateLdapBindDn(t *testing.T) {
 | 
				
			|||||||
			},
 | 
								},
 | 
				
			||||||
			loginSource: &models.LoginSource{
 | 
								loginSource: &models.LoginSource{
 | 
				
			||||||
				Type: models.LoginLDAP,
 | 
									Type: models.LoginLDAP,
 | 
				
			||||||
				Cfg: &models.LDAPConfig{
 | 
									Cfg: &ldap.Source{
 | 
				
			||||||
					Source: &ldap.Source{
 | 
					 | 
				
			||||||
					BindDN: "cn=readonly,dc=domain,dc=org",
 | 
										BindDN: "cn=readonly,dc=domain,dc=org",
 | 
				
			||||||
				},
 | 
									},
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		// case 18
 | 
							// case 18
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			args: []string{
 | 
								args: []string{
 | 
				
			||||||
@@ -839,13 +791,11 @@ func TestUpdateLdapBindDn(t *testing.T) {
 | 
				
			|||||||
			},
 | 
								},
 | 
				
			||||||
			loginSource: &models.LoginSource{
 | 
								loginSource: &models.LoginSource{
 | 
				
			||||||
				Type: models.LoginLDAP,
 | 
									Type: models.LoginLDAP,
 | 
				
			||||||
				Cfg: &models.LDAPConfig{
 | 
									Cfg: &ldap.Source{
 | 
				
			||||||
					Source: &ldap.Source{
 | 
					 | 
				
			||||||
					BindPassword: "secret",
 | 
										BindPassword: "secret",
 | 
				
			||||||
				},
 | 
									},
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		// case 19
 | 
							// case 19
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			args: []string{
 | 
								args: []string{
 | 
				
			||||||
@@ -856,9 +806,7 @@ func TestUpdateLdapBindDn(t *testing.T) {
 | 
				
			|||||||
			loginSource: &models.LoginSource{
 | 
								loginSource: &models.LoginSource{
 | 
				
			||||||
				Type:          models.LoginLDAP,
 | 
									Type:          models.LoginLDAP,
 | 
				
			||||||
				IsSyncEnabled: true,
 | 
									IsSyncEnabled: true,
 | 
				
			||||||
				Cfg: &models.LDAPConfig{
 | 
									Cfg:           &ldap.Source{},
 | 
				
			||||||
					Source: &ldap.Source{},
 | 
					 | 
				
			||||||
				},
 | 
					 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		// case 20
 | 
							// case 20
 | 
				
			||||||
@@ -870,13 +818,11 @@ func TestUpdateLdapBindDn(t *testing.T) {
 | 
				
			|||||||
			},
 | 
								},
 | 
				
			||||||
			loginSource: &models.LoginSource{
 | 
								loginSource: &models.LoginSource{
 | 
				
			||||||
				Type: models.LoginLDAP,
 | 
									Type: models.LoginLDAP,
 | 
				
			||||||
				Cfg: &models.LDAPConfig{
 | 
									Cfg: &ldap.Source{
 | 
				
			||||||
					Source: &ldap.Source{
 | 
					 | 
				
			||||||
					SearchPageSize: 12,
 | 
										SearchPageSize: 12,
 | 
				
			||||||
				},
 | 
									},
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		// case 21
 | 
							// case 21
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			args: []string{
 | 
								args: []string{
 | 
				
			||||||
@@ -901,9 +847,7 @@ func TestUpdateLdapBindDn(t *testing.T) {
 | 
				
			|||||||
			},
 | 
								},
 | 
				
			||||||
			existingLoginSource: &models.LoginSource{
 | 
								existingLoginSource: &models.LoginSource{
 | 
				
			||||||
				Type: models.LoginOAuth2,
 | 
									Type: models.LoginOAuth2,
 | 
				
			||||||
				Cfg: &models.LDAPConfig{
 | 
									Cfg:  &ldap.Source{},
 | 
				
			||||||
					Source: &ldap.Source{},
 | 
					 | 
				
			||||||
				},
 | 
					 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
			errMsg: "Invalid authentication type. expected: LDAP (via BindDN), actual: OAuth2",
 | 
								errMsg: "Invalid authentication type. expected: LDAP (via BindDN), actual: OAuth2",
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
@@ -933,9 +877,7 @@ func TestUpdateLdapBindDn(t *testing.T) {
 | 
				
			|||||||
				}
 | 
									}
 | 
				
			||||||
				return &models.LoginSource{
 | 
									return &models.LoginSource{
 | 
				
			||||||
					Type: models.LoginLDAP,
 | 
										Type: models.LoginLDAP,
 | 
				
			||||||
					Cfg: &models.LDAPConfig{
 | 
										Cfg:  &ldap.Source{},
 | 
				
			||||||
						Source: &ldap.Source{},
 | 
					 | 
				
			||||||
					},
 | 
					 | 
				
			||||||
				}, nil
 | 
									}, nil
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@@ -996,9 +938,8 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
 | 
				
			|||||||
			loginSource: &models.LoginSource{
 | 
								loginSource: &models.LoginSource{
 | 
				
			||||||
				Type:     models.LoginDLDAP,
 | 
									Type:     models.LoginDLDAP,
 | 
				
			||||||
				Name:     "ldap (simple auth) source full",
 | 
									Name:     "ldap (simple auth) source full",
 | 
				
			||||||
				IsActived: false,
 | 
									IsActive: false,
 | 
				
			||||||
				Cfg: &models.LDAPConfig{
 | 
									Cfg: &ldap.Source{
 | 
				
			||||||
					Source: &ldap.Source{
 | 
					 | 
				
			||||||
					Name:                  "ldap (simple auth) source full",
 | 
										Name:                  "ldap (simple auth) source full",
 | 
				
			||||||
					Host:                  "ldap-simple-server full",
 | 
										Host:                  "ldap-simple-server full",
 | 
				
			||||||
					Port:                  987,
 | 
										Port:                  987,
 | 
				
			||||||
@@ -1017,7 +958,6 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
 | 
				
			|||||||
				},
 | 
									},
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		// case 1
 | 
							// case 1
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			args: []string{
 | 
								args: []string{
 | 
				
			||||||
@@ -1026,9 +966,7 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
 | 
				
			|||||||
			},
 | 
								},
 | 
				
			||||||
			loginSource: &models.LoginSource{
 | 
								loginSource: &models.LoginSource{
 | 
				
			||||||
				Type: models.LoginDLDAP,
 | 
									Type: models.LoginDLDAP,
 | 
				
			||||||
				Cfg: &models.LDAPConfig{
 | 
									Cfg:  &ldap.Source{},
 | 
				
			||||||
					Source: &ldap.Source{},
 | 
					 | 
				
			||||||
				},
 | 
					 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		// case 2
 | 
							// case 2
 | 
				
			||||||
@@ -1041,13 +979,11 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
 | 
				
			|||||||
			loginSource: &models.LoginSource{
 | 
								loginSource: &models.LoginSource{
 | 
				
			||||||
				Type: models.LoginDLDAP,
 | 
									Type: models.LoginDLDAP,
 | 
				
			||||||
				Name: "ldap (simple auth) source",
 | 
									Name: "ldap (simple auth) source",
 | 
				
			||||||
				Cfg: &models.LDAPConfig{
 | 
									Cfg: &ldap.Source{
 | 
				
			||||||
					Source: &ldap.Source{
 | 
					 | 
				
			||||||
					Name: "ldap (simple auth) source",
 | 
										Name: "ldap (simple auth) source",
 | 
				
			||||||
				},
 | 
									},
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		// case 3
 | 
							// case 3
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			args: []string{
 | 
								args: []string{
 | 
				
			||||||
@@ -1057,17 +993,13 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
 | 
				
			|||||||
			},
 | 
								},
 | 
				
			||||||
			existingLoginSource: &models.LoginSource{
 | 
								existingLoginSource: &models.LoginSource{
 | 
				
			||||||
				Type:     models.LoginDLDAP,
 | 
									Type:     models.LoginDLDAP,
 | 
				
			||||||
				IsActived: true,
 | 
									IsActive: true,
 | 
				
			||||||
				Cfg: &models.LDAPConfig{
 | 
									Cfg:      &ldap.Source{},
 | 
				
			||||||
					Source: &ldap.Source{},
 | 
					 | 
				
			||||||
				},
 | 
					 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
			loginSource: &models.LoginSource{
 | 
								loginSource: &models.LoginSource{
 | 
				
			||||||
				Type:     models.LoginDLDAP,
 | 
									Type:     models.LoginDLDAP,
 | 
				
			||||||
				IsActived: false,
 | 
									IsActive: false,
 | 
				
			||||||
				Cfg: &models.LDAPConfig{
 | 
									Cfg:      &ldap.Source{},
 | 
				
			||||||
					Source: &ldap.Source{},
 | 
					 | 
				
			||||||
				},
 | 
					 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		// case 4
 | 
							// case 4
 | 
				
			||||||
@@ -1079,13 +1011,11 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
 | 
				
			|||||||
			},
 | 
								},
 | 
				
			||||||
			loginSource: &models.LoginSource{
 | 
								loginSource: &models.LoginSource{
 | 
				
			||||||
				Type: models.LoginDLDAP,
 | 
									Type: models.LoginDLDAP,
 | 
				
			||||||
				Cfg: &models.LDAPConfig{
 | 
									Cfg: &ldap.Source{
 | 
				
			||||||
					Source: &ldap.Source{
 | 
					 | 
				
			||||||
					SecurityProtocol: ldap.SecurityProtocol(2),
 | 
										SecurityProtocol: ldap.SecurityProtocol(2),
 | 
				
			||||||
				},
 | 
									},
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		// case 5
 | 
							// case 5
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			args: []string{
 | 
								args: []string{
 | 
				
			||||||
@@ -1095,13 +1025,11 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
 | 
				
			|||||||
			},
 | 
								},
 | 
				
			||||||
			loginSource: &models.LoginSource{
 | 
								loginSource: &models.LoginSource{
 | 
				
			||||||
				Type: models.LoginDLDAP,
 | 
									Type: models.LoginDLDAP,
 | 
				
			||||||
				Cfg: &models.LDAPConfig{
 | 
									Cfg: &ldap.Source{
 | 
				
			||||||
					Source: &ldap.Source{
 | 
					 | 
				
			||||||
					SkipVerify: true,
 | 
										SkipVerify: true,
 | 
				
			||||||
				},
 | 
									},
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		// case 6
 | 
							// case 6
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			args: []string{
 | 
								args: []string{
 | 
				
			||||||
@@ -1111,13 +1039,11 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
 | 
				
			|||||||
			},
 | 
								},
 | 
				
			||||||
			loginSource: &models.LoginSource{
 | 
								loginSource: &models.LoginSource{
 | 
				
			||||||
				Type: models.LoginDLDAP,
 | 
									Type: models.LoginDLDAP,
 | 
				
			||||||
				Cfg: &models.LDAPConfig{
 | 
									Cfg: &ldap.Source{
 | 
				
			||||||
					Source: &ldap.Source{
 | 
					 | 
				
			||||||
					Host: "ldap-server",
 | 
										Host: "ldap-server",
 | 
				
			||||||
				},
 | 
									},
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		// case 7
 | 
							// case 7
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			args: []string{
 | 
								args: []string{
 | 
				
			||||||
@@ -1127,13 +1053,11 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
 | 
				
			|||||||
			},
 | 
								},
 | 
				
			||||||
			loginSource: &models.LoginSource{
 | 
								loginSource: &models.LoginSource{
 | 
				
			||||||
				Type: models.LoginDLDAP,
 | 
									Type: models.LoginDLDAP,
 | 
				
			||||||
				Cfg: &models.LDAPConfig{
 | 
									Cfg: &ldap.Source{
 | 
				
			||||||
					Source: &ldap.Source{
 | 
					 | 
				
			||||||
					Port: 987,
 | 
										Port: 987,
 | 
				
			||||||
				},
 | 
									},
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		// case 8
 | 
							// case 8
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			args: []string{
 | 
								args: []string{
 | 
				
			||||||
@@ -1143,13 +1067,11 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
 | 
				
			|||||||
			},
 | 
								},
 | 
				
			||||||
			loginSource: &models.LoginSource{
 | 
								loginSource: &models.LoginSource{
 | 
				
			||||||
				Type: models.LoginDLDAP,
 | 
									Type: models.LoginDLDAP,
 | 
				
			||||||
				Cfg: &models.LDAPConfig{
 | 
									Cfg: &ldap.Source{
 | 
				
			||||||
					Source: &ldap.Source{
 | 
					 | 
				
			||||||
					UserBase: "ou=Users,dc=domain,dc=org",
 | 
										UserBase: "ou=Users,dc=domain,dc=org",
 | 
				
			||||||
				},
 | 
									},
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		// case 9
 | 
							// case 9
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			args: []string{
 | 
								args: []string{
 | 
				
			||||||
@@ -1159,13 +1081,11 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
 | 
				
			|||||||
			},
 | 
								},
 | 
				
			||||||
			loginSource: &models.LoginSource{
 | 
								loginSource: &models.LoginSource{
 | 
				
			||||||
				Type: models.LoginDLDAP,
 | 
									Type: models.LoginDLDAP,
 | 
				
			||||||
				Cfg: &models.LDAPConfig{
 | 
									Cfg: &ldap.Source{
 | 
				
			||||||
					Source: &ldap.Source{
 | 
					 | 
				
			||||||
					Filter: "(&(objectClass=posixAccount)(cn=%s))",
 | 
										Filter: "(&(objectClass=posixAccount)(cn=%s))",
 | 
				
			||||||
				},
 | 
									},
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		// case 10
 | 
							// case 10
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			args: []string{
 | 
								args: []string{
 | 
				
			||||||
@@ -1175,13 +1095,11 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
 | 
				
			|||||||
			},
 | 
								},
 | 
				
			||||||
			loginSource: &models.LoginSource{
 | 
								loginSource: &models.LoginSource{
 | 
				
			||||||
				Type: models.LoginDLDAP,
 | 
									Type: models.LoginDLDAP,
 | 
				
			||||||
				Cfg: &models.LDAPConfig{
 | 
									Cfg: &ldap.Source{
 | 
				
			||||||
					Source: &ldap.Source{
 | 
					 | 
				
			||||||
					AdminFilter: "(memberOf=cn=admin-group,ou=example,dc=domain,dc=org)",
 | 
										AdminFilter: "(memberOf=cn=admin-group,ou=example,dc=domain,dc=org)",
 | 
				
			||||||
				},
 | 
									},
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		// case 11
 | 
							// case 11
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			args: []string{
 | 
								args: []string{
 | 
				
			||||||
@@ -1191,13 +1109,11 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
 | 
				
			|||||||
			},
 | 
								},
 | 
				
			||||||
			loginSource: &models.LoginSource{
 | 
								loginSource: &models.LoginSource{
 | 
				
			||||||
				Type: models.LoginDLDAP,
 | 
									Type: models.LoginDLDAP,
 | 
				
			||||||
				Cfg: &models.LDAPConfig{
 | 
									Cfg: &ldap.Source{
 | 
				
			||||||
					Source: &ldap.Source{
 | 
					 | 
				
			||||||
					AttributeUsername: "uid",
 | 
										AttributeUsername: "uid",
 | 
				
			||||||
				},
 | 
									},
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		// case 12
 | 
							// case 12
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			args: []string{
 | 
								args: []string{
 | 
				
			||||||
@@ -1207,13 +1123,11 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
 | 
				
			|||||||
			},
 | 
								},
 | 
				
			||||||
			loginSource: &models.LoginSource{
 | 
								loginSource: &models.LoginSource{
 | 
				
			||||||
				Type: models.LoginDLDAP,
 | 
									Type: models.LoginDLDAP,
 | 
				
			||||||
				Cfg: &models.LDAPConfig{
 | 
									Cfg: &ldap.Source{
 | 
				
			||||||
					Source: &ldap.Source{
 | 
					 | 
				
			||||||
					AttributeName: "givenName",
 | 
										AttributeName: "givenName",
 | 
				
			||||||
				},
 | 
									},
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		// case 13
 | 
							// case 13
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			args: []string{
 | 
								args: []string{
 | 
				
			||||||
@@ -1223,13 +1137,11 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
 | 
				
			|||||||
			},
 | 
								},
 | 
				
			||||||
			loginSource: &models.LoginSource{
 | 
								loginSource: &models.LoginSource{
 | 
				
			||||||
				Type: models.LoginDLDAP,
 | 
									Type: models.LoginDLDAP,
 | 
				
			||||||
				Cfg: &models.LDAPConfig{
 | 
									Cfg: &ldap.Source{
 | 
				
			||||||
					Source: &ldap.Source{
 | 
					 | 
				
			||||||
					AttributeSurname: "sn",
 | 
										AttributeSurname: "sn",
 | 
				
			||||||
				},
 | 
									},
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		// case 14
 | 
							// case 14
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			args: []string{
 | 
								args: []string{
 | 
				
			||||||
@@ -1239,13 +1151,12 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
 | 
				
			|||||||
			},
 | 
								},
 | 
				
			||||||
			loginSource: &models.LoginSource{
 | 
								loginSource: &models.LoginSource{
 | 
				
			||||||
				Type: models.LoginDLDAP,
 | 
									Type: models.LoginDLDAP,
 | 
				
			||||||
				Cfg: &models.LDAPConfig{
 | 
									Cfg: &ldap.Source{
 | 
				
			||||||
					Source: &ldap.Source{
 | 
					
 | 
				
			||||||
					AttributeMail: "mail",
 | 
										AttributeMail: "mail",
 | 
				
			||||||
				},
 | 
									},
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		// case 15
 | 
							// case 15
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			args: []string{
 | 
								args: []string{
 | 
				
			||||||
@@ -1255,13 +1166,11 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
 | 
				
			|||||||
			},
 | 
								},
 | 
				
			||||||
			loginSource: &models.LoginSource{
 | 
								loginSource: &models.LoginSource{
 | 
				
			||||||
				Type: models.LoginDLDAP,
 | 
									Type: models.LoginDLDAP,
 | 
				
			||||||
				Cfg: &models.LDAPConfig{
 | 
									Cfg: &ldap.Source{
 | 
				
			||||||
					Source: &ldap.Source{
 | 
					 | 
				
			||||||
					AttributeSSHPublicKey: "publickey",
 | 
										AttributeSSHPublicKey: "publickey",
 | 
				
			||||||
				},
 | 
									},
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		// case 16
 | 
							// case 16
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			args: []string{
 | 
								args: []string{
 | 
				
			||||||
@@ -1271,13 +1180,11 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
 | 
				
			|||||||
			},
 | 
								},
 | 
				
			||||||
			loginSource: &models.LoginSource{
 | 
								loginSource: &models.LoginSource{
 | 
				
			||||||
				Type: models.LoginDLDAP,
 | 
									Type: models.LoginDLDAP,
 | 
				
			||||||
				Cfg: &models.LDAPConfig{
 | 
									Cfg: &ldap.Source{
 | 
				
			||||||
					Source: &ldap.Source{
 | 
					 | 
				
			||||||
					UserDN: "cn=%s,ou=Users,dc=domain,dc=org",
 | 
										UserDN: "cn=%s,ou=Users,dc=domain,dc=org",
 | 
				
			||||||
				},
 | 
									},
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		// case 17
 | 
							// case 17
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			args: []string{
 | 
								args: []string{
 | 
				
			||||||
@@ -1302,9 +1209,7 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
 | 
				
			|||||||
			},
 | 
								},
 | 
				
			||||||
			existingLoginSource: &models.LoginSource{
 | 
								existingLoginSource: &models.LoginSource{
 | 
				
			||||||
				Type: models.LoginPAM,
 | 
									Type: models.LoginPAM,
 | 
				
			||||||
				Cfg: &models.LDAPConfig{
 | 
									Cfg:  &ldap.Source{},
 | 
				
			||||||
					Source: &ldap.Source{},
 | 
					 | 
				
			||||||
				},
 | 
					 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
			errMsg: "Invalid authentication type. expected: LDAP (simple auth), actual: PAM",
 | 
								errMsg: "Invalid authentication type. expected: LDAP (simple auth), actual: PAM",
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
@@ -1334,9 +1239,7 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
 | 
				
			|||||||
				}
 | 
									}
 | 
				
			||||||
				return &models.LoginSource{
 | 
									return &models.LoginSource{
 | 
				
			||||||
					Type: models.LoginDLDAP,
 | 
										Type: models.LoginDLDAP,
 | 
				
			||||||
					Cfg: &models.LDAPConfig{
 | 
										Cfg:  &ldap.Source{},
 | 
				
			||||||
						Source: &ldap.Source{},
 | 
					 | 
				
			||||||
					},
 | 
					 | 
				
			||||||
				}, nil
 | 
									}, nil
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -11,7 +11,7 @@ import (
 | 
				
			|||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
	"testing"
 | 
						"testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"code.gitea.io/gitea/models"
 | 
						"code.gitea.io/gitea/services/auth"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"github.com/stretchr/testify/assert"
 | 
						"github.com/stretchr/testify/assert"
 | 
				
			||||||
	"github.com/unknwon/i18n"
 | 
						"github.com/unknwon/i18n"
 | 
				
			||||||
@@ -205,7 +205,7 @@ func TestLDAPUserSync(t *testing.T) {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
	defer prepareTestEnv(t)()
 | 
						defer prepareTestEnv(t)()
 | 
				
			||||||
	addAuthSourceLDAP(t, "")
 | 
						addAuthSourceLDAP(t, "")
 | 
				
			||||||
	models.SyncExternalUsers(context.Background(), true)
 | 
						auth.SyncExternalUsers(context.Background(), true)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	session := loginUser(t, "user1")
 | 
						session := loginUser(t, "user1")
 | 
				
			||||||
	// Check if users exists
 | 
						// Check if users exists
 | 
				
			||||||
@@ -270,7 +270,7 @@ func TestLDAPUserSSHKeySync(t *testing.T) {
 | 
				
			|||||||
	defer prepareTestEnv(t)()
 | 
						defer prepareTestEnv(t)()
 | 
				
			||||||
	addAuthSourceLDAP(t, "sshPublicKey")
 | 
						addAuthSourceLDAP(t, "sshPublicKey")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	models.SyncExternalUsers(context.Background(), true)
 | 
						auth.SyncExternalUsers(context.Background(), true)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Check if users has SSH keys synced
 | 
						// Check if users has SSH keys synced
 | 
				
			||||||
	for _, u := range gitLDAPUsers {
 | 
						for _, u := range gitLDAPUsers {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,6 +4,12 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
package models
 | 
					package models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"encoding/binary"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						jsoniter "github.com/json-iterator/go"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func keysInt64(m map[int64]struct{}) []int64 {
 | 
					func keysInt64(m map[int64]struct{}) []int64 {
 | 
				
			||||||
	keys := make([]int64, 0, len(m))
 | 
						keys := make([]int64, 0, len(m))
 | 
				
			||||||
	for k := range m {
 | 
						for k := range m {
 | 
				
			||||||
@@ -27,3 +33,33 @@ func valuesUser(m map[int64]*User) []*User {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
	return values
 | 
						return values
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// JSONUnmarshalHandleDoubleEncode - due to a bug in xorm (see https://gitea.com/xorm/xorm/pulls/1957) - it's
 | 
				
			||||||
 | 
					// possible that a Blob may be double encoded or gain an unwanted prefix of 0xff 0xfe.
 | 
				
			||||||
 | 
					func JSONUnmarshalHandleDoubleEncode(bs []byte, v interface{}) error {
 | 
				
			||||||
 | 
						json := jsoniter.ConfigCompatibleWithStandardLibrary
 | 
				
			||||||
 | 
						err := json.Unmarshal(bs, v)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							ok := true
 | 
				
			||||||
 | 
							rs := []byte{}
 | 
				
			||||||
 | 
							temp := make([]byte, 2)
 | 
				
			||||||
 | 
							for _, rn := range string(bs) {
 | 
				
			||||||
 | 
								if rn > 0xffff {
 | 
				
			||||||
 | 
									ok = false
 | 
				
			||||||
 | 
									break
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								binary.LittleEndian.PutUint16(temp, uint16(rn))
 | 
				
			||||||
 | 
								rs = append(rs, temp...)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if ok {
 | 
				
			||||||
 | 
								if rs[0] == 0xff && rs[1] == 0xfe {
 | 
				
			||||||
 | 
									rs = rs[2:]
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								err = json.Unmarshal(rs, v)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if err != nil && len(bs) > 2 && bs[0] == 0xff && bs[1] == 0xfe {
 | 
				
			||||||
 | 
							err = json.Unmarshal(bs[2:], v)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,25 +6,11 @@
 | 
				
			|||||||
package models
 | 
					package models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"crypto/tls"
 | 
						"reflect"
 | 
				
			||||||
	"encoding/binary"
 | 
					 | 
				
			||||||
	"errors"
 | 
					 | 
				
			||||||
	"fmt"
 | 
					 | 
				
			||||||
	"net/smtp"
 | 
					 | 
				
			||||||
	"net/textproto"
 | 
					 | 
				
			||||||
	"strconv"
 | 
						"strconv"
 | 
				
			||||||
	"strings"
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"code.gitea.io/gitea/modules/auth/ldap"
 | 
					 | 
				
			||||||
	"code.gitea.io/gitea/modules/auth/oauth2"
 | 
					 | 
				
			||||||
	"code.gitea.io/gitea/modules/auth/pam"
 | 
					 | 
				
			||||||
	"code.gitea.io/gitea/modules/log"
 | 
						"code.gitea.io/gitea/modules/log"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/secret"
 | 
					 | 
				
			||||||
	"code.gitea.io/gitea/modules/setting"
 | 
					 | 
				
			||||||
	"code.gitea.io/gitea/modules/timeutil"
 | 
						"code.gitea.io/gitea/modules/timeutil"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/util"
 | 
					 | 
				
			||||||
	gouuid "github.com/google/uuid"
 | 
					 | 
				
			||||||
	jsoniter "github.com/json-iterator/go"
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"xorm.io/xorm"
 | 
						"xorm.io/xorm"
 | 
				
			||||||
	"xorm.io/xorm/convert"
 | 
						"xorm.io/xorm/convert"
 | 
				
			||||||
@@ -45,6 +31,11 @@ const (
 | 
				
			|||||||
	LoginSSPI             // 7
 | 
						LoginSSPI             // 7
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// String returns the string name of the LoginType
 | 
				
			||||||
 | 
					func (typ LoginType) String() string {
 | 
				
			||||||
 | 
						return LoginNames[typ]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// LoginNames contains the name of LoginType values.
 | 
					// LoginNames contains the name of LoginType values.
 | 
				
			||||||
var LoginNames = map[LoginType]string{
 | 
					var LoginNames = map[LoginType]string{
 | 
				
			||||||
	LoginLDAP:   "LDAP (via BindDN)",
 | 
						LoginLDAP:   "LDAP (via BindDN)",
 | 
				
			||||||
@@ -55,173 +46,66 @@ var LoginNames = map[LoginType]string{
 | 
				
			|||||||
	LoginSSPI:   "SPNEGO with SSPI",
 | 
						LoginSSPI:   "SPNEGO with SSPI",
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// SecurityProtocolNames contains the name of SecurityProtocol values.
 | 
					// LoginConfig represents login config as far as the db is concerned
 | 
				
			||||||
var SecurityProtocolNames = map[ldap.SecurityProtocol]string{
 | 
					type LoginConfig interface {
 | 
				
			||||||
	ldap.SecurityProtocolUnencrypted: "Unencrypted",
 | 
						convert.Conversion
 | 
				
			||||||
	ldap.SecurityProtocolLDAPS:       "LDAPS",
 | 
					 | 
				
			||||||
	ldap.SecurityProtocolStartTLS:    "StartTLS",
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Ensure structs implemented interface.
 | 
					// SkipVerifiable configurations provide a IsSkipVerify to check if SkipVerify is set
 | 
				
			||||||
var (
 | 
					type SkipVerifiable interface {
 | 
				
			||||||
	_ convert.Conversion = &LDAPConfig{}
 | 
						IsSkipVerify() bool
 | 
				
			||||||
	_ convert.Conversion = &SMTPConfig{}
 | 
					 | 
				
			||||||
	_ convert.Conversion = &PAMConfig{}
 | 
					 | 
				
			||||||
	_ convert.Conversion = &OAuth2Config{}
 | 
					 | 
				
			||||||
	_ convert.Conversion = &SSPIConfig{}
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// jsonUnmarshalHandleDoubleEncode - due to a bug in xorm (see https://gitea.com/xorm/xorm/pulls/1957) - it's
 | 
					 | 
				
			||||||
// possible that a Blob may be double encoded or gain an unwanted prefix of 0xff 0xfe.
 | 
					 | 
				
			||||||
func jsonUnmarshalHandleDoubleEncode(bs []byte, v interface{}) error {
 | 
					 | 
				
			||||||
	json := jsoniter.ConfigCompatibleWithStandardLibrary
 | 
					 | 
				
			||||||
	err := json.Unmarshal(bs, v)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		ok := true
 | 
					 | 
				
			||||||
		rs := []byte{}
 | 
					 | 
				
			||||||
		temp := make([]byte, 2)
 | 
					 | 
				
			||||||
		for _, rn := range string(bs) {
 | 
					 | 
				
			||||||
			if rn > 0xffff {
 | 
					 | 
				
			||||||
				ok = false
 | 
					 | 
				
			||||||
				break
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			binary.LittleEndian.PutUint16(temp, uint16(rn))
 | 
					 | 
				
			||||||
			rs = append(rs, temp...)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		if ok {
 | 
					 | 
				
			||||||
			if rs[0] == 0xff && rs[1] == 0xfe {
 | 
					 | 
				
			||||||
				rs = rs[2:]
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			err = json.Unmarshal(rs, v)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	if err != nil && len(bs) > 2 && bs[0] == 0xff && bs[1] == 0xfe {
 | 
					 | 
				
			||||||
		err = json.Unmarshal(bs[2:], v)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return err
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// LDAPConfig holds configuration for LDAP login source.
 | 
					// HasTLSer configurations provide a HasTLS to check if TLS can be enabled
 | 
				
			||||||
type LDAPConfig struct {
 | 
					type HasTLSer interface {
 | 
				
			||||||
	*ldap.Source
 | 
						HasTLS() bool
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// FromDB fills up a LDAPConfig from serialized format.
 | 
					// UseTLSer configurations provide a HasTLS to check if TLS is enabled
 | 
				
			||||||
func (cfg *LDAPConfig) FromDB(bs []byte) error {
 | 
					type UseTLSer interface {
 | 
				
			||||||
	err := jsonUnmarshalHandleDoubleEncode(bs, &cfg)
 | 
						UseTLS() bool
 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		return err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	if cfg.BindPasswordEncrypt != "" {
 | 
					 | 
				
			||||||
		cfg.BindPassword, err = secret.DecryptSecret(setting.SecretKey, cfg.BindPasswordEncrypt)
 | 
					 | 
				
			||||||
		cfg.BindPasswordEncrypt = ""
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return err
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// ToDB exports a LDAPConfig to a serialized format.
 | 
					// SSHKeyProvider configurations provide ProvidesSSHKeys to check if they provide SSHKeys
 | 
				
			||||||
func (cfg *LDAPConfig) ToDB() ([]byte, error) {
 | 
					type SSHKeyProvider interface {
 | 
				
			||||||
	var err error
 | 
						ProvidesSSHKeys() bool
 | 
				
			||||||
	cfg.BindPasswordEncrypt, err = secret.EncryptSecret(setting.SecretKey, cfg.BindPassword)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		return nil, err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	cfg.BindPassword = ""
 | 
					 | 
				
			||||||
	json := jsoniter.ConfigCompatibleWithStandardLibrary
 | 
					 | 
				
			||||||
	return json.Marshal(cfg)
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// SecurityProtocolName returns the name of configured security
 | 
					// RegisterableSource configurations provide RegisterSource which needs to be run on creation
 | 
				
			||||||
// protocol.
 | 
					type RegisterableSource interface {
 | 
				
			||||||
func (cfg *LDAPConfig) SecurityProtocolName() string {
 | 
						RegisterSource() error
 | 
				
			||||||
	return SecurityProtocolNames[cfg.SecurityProtocol]
 | 
						UnregisterSource() error
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// SMTPConfig holds configuration for the SMTP login source.
 | 
					// LoginSourceSettable configurations can have their loginSource set on them
 | 
				
			||||||
type SMTPConfig struct {
 | 
					type LoginSourceSettable interface {
 | 
				
			||||||
	Auth           string
 | 
						SetLoginSource(*LoginSource)
 | 
				
			||||||
	Host           string
 | 
					 | 
				
			||||||
	Port           int
 | 
					 | 
				
			||||||
	AllowedDomains string `xorm:"TEXT"`
 | 
					 | 
				
			||||||
	TLS            bool
 | 
					 | 
				
			||||||
	SkipVerify     bool
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// FromDB fills up an SMTPConfig from serialized format.
 | 
					// RegisterLoginTypeConfig register a config for a provided type
 | 
				
			||||||
func (cfg *SMTPConfig) FromDB(bs []byte) error {
 | 
					func RegisterLoginTypeConfig(typ LoginType, exemplar LoginConfig) {
 | 
				
			||||||
	return jsonUnmarshalHandleDoubleEncode(bs, cfg)
 | 
						if reflect.TypeOf(exemplar).Kind() == reflect.Ptr {
 | 
				
			||||||
 | 
							// Pointer:
 | 
				
			||||||
 | 
							registeredLoginConfigs[typ] = func() LoginConfig {
 | 
				
			||||||
 | 
								return reflect.New(reflect.ValueOf(exemplar).Elem().Type()).Interface().(LoginConfig)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// ToDB exports an SMTPConfig to a serialized format.
 | 
						// Not a Pointer
 | 
				
			||||||
func (cfg *SMTPConfig) ToDB() ([]byte, error) {
 | 
						registeredLoginConfigs[typ] = func() LoginConfig {
 | 
				
			||||||
	json := jsoniter.ConfigCompatibleWithStandardLibrary
 | 
							return reflect.New(reflect.TypeOf(exemplar)).Elem().Interface().(LoginConfig)
 | 
				
			||||||
	return json.Marshal(cfg)
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// PAMConfig holds configuration for the PAM login source.
 | 
					var registeredLoginConfigs = map[LoginType]func() LoginConfig{}
 | 
				
			||||||
type PAMConfig struct {
 | 
					 | 
				
			||||||
	ServiceName string // pam service (e.g. system-auth)
 | 
					 | 
				
			||||||
	EmailDomain string
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// FromDB fills up a PAMConfig from serialized format.
 | 
					 | 
				
			||||||
func (cfg *PAMConfig) FromDB(bs []byte) error {
 | 
					 | 
				
			||||||
	return jsonUnmarshalHandleDoubleEncode(bs, cfg)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// ToDB exports a PAMConfig to a serialized format.
 | 
					 | 
				
			||||||
func (cfg *PAMConfig) ToDB() ([]byte, error) {
 | 
					 | 
				
			||||||
	json := jsoniter.ConfigCompatibleWithStandardLibrary
 | 
					 | 
				
			||||||
	return json.Marshal(cfg)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// OAuth2Config holds configuration for the OAuth2 login source.
 | 
					 | 
				
			||||||
type OAuth2Config struct {
 | 
					 | 
				
			||||||
	Provider                      string
 | 
					 | 
				
			||||||
	ClientID                      string
 | 
					 | 
				
			||||||
	ClientSecret                  string
 | 
					 | 
				
			||||||
	OpenIDConnectAutoDiscoveryURL string
 | 
					 | 
				
			||||||
	CustomURLMapping              *oauth2.CustomURLMapping
 | 
					 | 
				
			||||||
	IconURL                       string
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// FromDB fills up an OAuth2Config from serialized format.
 | 
					 | 
				
			||||||
func (cfg *OAuth2Config) FromDB(bs []byte) error {
 | 
					 | 
				
			||||||
	return jsonUnmarshalHandleDoubleEncode(bs, cfg)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// ToDB exports an SMTPConfig to a serialized format.
 | 
					 | 
				
			||||||
func (cfg *OAuth2Config) ToDB() ([]byte, error) {
 | 
					 | 
				
			||||||
	json := jsoniter.ConfigCompatibleWithStandardLibrary
 | 
					 | 
				
			||||||
	return json.Marshal(cfg)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// SSPIConfig holds configuration for SSPI single sign-on.
 | 
					 | 
				
			||||||
type SSPIConfig struct {
 | 
					 | 
				
			||||||
	AutoCreateUsers      bool
 | 
					 | 
				
			||||||
	AutoActivateUsers    bool
 | 
					 | 
				
			||||||
	StripDomainNames     bool
 | 
					 | 
				
			||||||
	SeparatorReplacement string
 | 
					 | 
				
			||||||
	DefaultLanguage      string
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// FromDB fills up an SSPIConfig from serialized format.
 | 
					 | 
				
			||||||
func (cfg *SSPIConfig) FromDB(bs []byte) error {
 | 
					 | 
				
			||||||
	return jsonUnmarshalHandleDoubleEncode(bs, cfg)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// ToDB exports an SSPIConfig to a serialized format.
 | 
					 | 
				
			||||||
func (cfg *SSPIConfig) ToDB() ([]byte, error) {
 | 
					 | 
				
			||||||
	json := jsoniter.ConfigCompatibleWithStandardLibrary
 | 
					 | 
				
			||||||
	return json.Marshal(cfg)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
// LoginSource represents an external way for authorizing users.
 | 
					// LoginSource represents an external way for authorizing users.
 | 
				
			||||||
type LoginSource struct {
 | 
					type LoginSource struct {
 | 
				
			||||||
	ID            int64 `xorm:"pk autoincr"`
 | 
						ID            int64 `xorm:"pk autoincr"`
 | 
				
			||||||
	Type          LoginType
 | 
						Type          LoginType
 | 
				
			||||||
	Name          string             `xorm:"UNIQUE"`
 | 
						Name          string             `xorm:"UNIQUE"`
 | 
				
			||||||
	IsActived     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"`
 | 
						Cfg           convert.Conversion `xorm:"TEXT"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -245,19 +129,14 @@ func Cell2Int64(val xorm.Cell) int64 {
 | 
				
			|||||||
// BeforeSet is invoked from XORM before setting the value of a field of this object.
 | 
					// BeforeSet is invoked from XORM before setting the value of a field of this object.
 | 
				
			||||||
func (source *LoginSource) BeforeSet(colName string, val xorm.Cell) {
 | 
					func (source *LoginSource) BeforeSet(colName string, val xorm.Cell) {
 | 
				
			||||||
	if colName == "type" {
 | 
						if colName == "type" {
 | 
				
			||||||
		switch LoginType(Cell2Int64(val)) {
 | 
							typ := LoginType(Cell2Int64(val))
 | 
				
			||||||
		case LoginLDAP, LoginDLDAP:
 | 
							constructor, ok := registeredLoginConfigs[typ]
 | 
				
			||||||
			source.Cfg = new(LDAPConfig)
 | 
							if !ok {
 | 
				
			||||||
		case LoginSMTP:
 | 
								return
 | 
				
			||||||
			source.Cfg = new(SMTPConfig)
 | 
							}
 | 
				
			||||||
		case LoginPAM:
 | 
							source.Cfg = constructor()
 | 
				
			||||||
			source.Cfg = new(PAMConfig)
 | 
							if settable, ok := source.Cfg.(LoginSourceSettable); ok {
 | 
				
			||||||
		case LoginOAuth2:
 | 
								settable.SetLoginSource(source)
 | 
				
			||||||
			source.Cfg = new(OAuth2Config)
 | 
					 | 
				
			||||||
		case LoginSSPI:
 | 
					 | 
				
			||||||
			source.Cfg = new(SSPIConfig)
 | 
					 | 
				
			||||||
		default:
 | 
					 | 
				
			||||||
			panic(fmt.Sprintf("unrecognized login source type: %v", *val))
 | 
					 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -299,59 +178,21 @@ func (source *LoginSource) IsSSPI() bool {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// HasTLS returns true of this source supports TLS.
 | 
					// HasTLS returns true of this source supports TLS.
 | 
				
			||||||
func (source *LoginSource) HasTLS() bool {
 | 
					func (source *LoginSource) HasTLS() bool {
 | 
				
			||||||
	return ((source.IsLDAP() || source.IsDLDAP()) &&
 | 
						hasTLSer, ok := source.Cfg.(HasTLSer)
 | 
				
			||||||
		source.LDAP().SecurityProtocol > ldap.SecurityProtocolUnencrypted) ||
 | 
						return ok && hasTLSer.HasTLS()
 | 
				
			||||||
		source.IsSMTP()
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// UseTLS returns true of this source is configured to use TLS.
 | 
					// UseTLS returns true of this source is configured to use TLS.
 | 
				
			||||||
func (source *LoginSource) UseTLS() bool {
 | 
					func (source *LoginSource) UseTLS() bool {
 | 
				
			||||||
	switch source.Type {
 | 
						useTLSer, ok := source.Cfg.(UseTLSer)
 | 
				
			||||||
	case LoginLDAP, LoginDLDAP:
 | 
						return ok && useTLSer.UseTLS()
 | 
				
			||||||
		return source.LDAP().SecurityProtocol != ldap.SecurityProtocolUnencrypted
 | 
					 | 
				
			||||||
	case LoginSMTP:
 | 
					 | 
				
			||||||
		return source.SMTP().TLS
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return false
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// SkipVerify returns true if this source is configured to skip SSL
 | 
					// SkipVerify returns true if this source is configured to skip SSL
 | 
				
			||||||
// verification.
 | 
					// verification.
 | 
				
			||||||
func (source *LoginSource) SkipVerify() bool {
 | 
					func (source *LoginSource) SkipVerify() bool {
 | 
				
			||||||
	switch source.Type {
 | 
						skipVerifiable, ok := source.Cfg.(SkipVerifiable)
 | 
				
			||||||
	case LoginLDAP, LoginDLDAP:
 | 
						return ok && skipVerifiable.IsSkipVerify()
 | 
				
			||||||
		return source.LDAP().SkipVerify
 | 
					 | 
				
			||||||
	case LoginSMTP:
 | 
					 | 
				
			||||||
		return source.SMTP().SkipVerify
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return false
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// LDAP returns LDAPConfig for this source, if of LDAP type.
 | 
					 | 
				
			||||||
func (source *LoginSource) LDAP() *LDAPConfig {
 | 
					 | 
				
			||||||
	return source.Cfg.(*LDAPConfig)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// SMTP returns SMTPConfig for this source, if of SMTP type.
 | 
					 | 
				
			||||||
func (source *LoginSource) SMTP() *SMTPConfig {
 | 
					 | 
				
			||||||
	return source.Cfg.(*SMTPConfig)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// PAM returns PAMConfig for this source, if of PAM type.
 | 
					 | 
				
			||||||
func (source *LoginSource) PAM() *PAMConfig {
 | 
					 | 
				
			||||||
	return source.Cfg.(*PAMConfig)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// OAuth2 returns OAuth2Config for this source, if of OAuth2 type.
 | 
					 | 
				
			||||||
func (source *LoginSource) OAuth2() *OAuth2Config {
 | 
					 | 
				
			||||||
	return source.Cfg.(*OAuth2Config)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// SSPI returns SSPIConfig for this source, if of SSPI type.
 | 
					 | 
				
			||||||
func (source *LoginSource) SSPI() *SSPIConfig {
 | 
					 | 
				
			||||||
	return source.Cfg.(*SSPIConfig)
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// CreateLoginSource inserts a LoginSource in the DB if not already
 | 
					// CreateLoginSource inserts a LoginSource in the DB if not already
 | 
				
			||||||
@@ -369,17 +210,25 @@ func CreateLoginSource(source *LoginSource) error {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	_, err = x.Insert(source)
 | 
						_, err = x.Insert(source)
 | 
				
			||||||
	if err == nil && source.IsOAuth2() && source.IsActived {
 | 
					 | 
				
			||||||
		oAuth2Config := source.OAuth2()
 | 
					 | 
				
			||||||
		err = oauth2.RegisterProvider(source.Name, oAuth2Config.Provider, oAuth2Config.ClientID, oAuth2Config.ClientSecret, oAuth2Config.OpenIDConnectAutoDiscoveryURL, oAuth2Config.CustomURLMapping)
 | 
					 | 
				
			||||||
		err = wrapOpenIDConnectInitializeError(err, source.Name, oAuth2Config)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
			// remove the LoginSource in case of errors while registering OAuth2 providers
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if !source.IsActive {
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						registerableSource, ok := source.Cfg.(RegisterableSource)
 | 
				
			||||||
 | 
						if !ok {
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						err = registerableSource.RegisterSource()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							// remove the LoginSource in case of errors while registering configuration
 | 
				
			||||||
		if _, err := x.Delete(source); err != nil {
 | 
							if _, err := x.Delete(source); err != nil {
 | 
				
			||||||
			log.Error("CreateLoginSource: Error while wrapOpenIDConnectInitializeError: %v", err)
 | 
								log.Error("CreateLoginSource: Error while wrapOpenIDConnectInitializeError: %v", err)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
			return err
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	return err
 | 
						return err
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -399,10 +248,19 @@ func LoginSourcesByType(loginType LoginType) ([]*LoginSource, error) {
 | 
				
			|||||||
	return sources, nil
 | 
						return sources, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// AllActiveLoginSources returns all active sources
 | 
				
			||||||
 | 
					func AllActiveLoginSources() ([]*LoginSource, error) {
 | 
				
			||||||
 | 
						sources := make([]*LoginSource, 0, 5)
 | 
				
			||||||
 | 
						if err := x.Where("is_active = ?", true).Find(&sources); err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return sources, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// ActiveLoginSources returns all active sources of the specified type
 | 
					// ActiveLoginSources returns all active sources of the specified type
 | 
				
			||||||
func ActiveLoginSources(loginType LoginType) ([]*LoginSource, error) {
 | 
					func ActiveLoginSources(loginType LoginType) ([]*LoginSource, error) {
 | 
				
			||||||
	sources := make([]*LoginSource, 0, 1)
 | 
						sources := make([]*LoginSource, 0, 1)
 | 
				
			||||||
	if err := x.Where("is_actived = ? and type = ?", true, loginType).Find(&sources); err != nil {
 | 
						if err := x.Where("is_active = ? and type = ?", true, loginType).Find(&sources); err != nil {
 | 
				
			||||||
		return nil, err
 | 
							return nil, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	return sources, nil
 | 
						return sources, nil
 | 
				
			||||||
@@ -425,6 +283,14 @@ func IsSSPIEnabled() bool {
 | 
				
			|||||||
// GetLoginSourceByID returns login source by given ID.
 | 
					// GetLoginSourceByID returns login source by given ID.
 | 
				
			||||||
func GetLoginSourceByID(id int64) (*LoginSource, error) {
 | 
					func GetLoginSourceByID(id int64) (*LoginSource, error) {
 | 
				
			||||||
	source := new(LoginSource)
 | 
						source := new(LoginSource)
 | 
				
			||||||
 | 
						if id == 0 {
 | 
				
			||||||
 | 
							source.Cfg = registeredLoginConfigs[LoginNoType]()
 | 
				
			||||||
 | 
							// Set this source to active
 | 
				
			||||||
 | 
							// FIXME: allow disabling of db based password authentication in future
 | 
				
			||||||
 | 
							source.IsActive = true
 | 
				
			||||||
 | 
							return source, nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	has, err := x.ID(id).Get(source)
 | 
						has, err := x.ID(id).Get(source)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return nil, err
 | 
							return nil, err
 | 
				
			||||||
@@ -446,17 +312,25 @@ func UpdateSource(source *LoginSource) error {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	_, err := x.ID(source.ID).AllCols().Update(source)
 | 
						_, err := x.ID(source.ID).AllCols().Update(source)
 | 
				
			||||||
	if err == nil && source.IsOAuth2() && source.IsActived {
 | 
						if err != nil {
 | 
				
			||||||
		oAuth2Config := source.OAuth2()
 | 
							return err
 | 
				
			||||||
		err = oauth2.RegisterProvider(source.Name, oAuth2Config.Provider, oAuth2Config.ClientID, oAuth2Config.ClientSecret, oAuth2Config.OpenIDConnectAutoDiscoveryURL, oAuth2Config.CustomURLMapping)
 | 
						}
 | 
				
			||||||
		err = wrapOpenIDConnectInitializeError(err, source.Name, oAuth2Config)
 | 
					
 | 
				
			||||||
 | 
						if !source.IsActive {
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						registerableSource, ok := source.Cfg.(RegisterableSource)
 | 
				
			||||||
 | 
						if !ok {
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						err = registerableSource.RegisterSource()
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		// restore original values since we cannot update the provider it self
 | 
							// restore original values since we cannot update the provider it self
 | 
				
			||||||
		if _, err := x.ID(source.ID).AllCols().Update(originalLoginSource); err != nil {
 | 
							if _, err := x.ID(source.ID).AllCols().Update(originalLoginSource); err != nil {
 | 
				
			||||||
			log.Error("UpdateSource: Error while wrapOpenIDConnectInitializeError: %v", err)
 | 
								log.Error("UpdateSource: Error while wrapOpenIDConnectInitializeError: %v", err)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
			return err
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	return err
 | 
						return err
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -477,8 +351,10 @@ func DeleteSource(source *LoginSource) error {
 | 
				
			|||||||
		return ErrLoginSourceInUse{source.ID}
 | 
							return ErrLoginSourceInUse{source.ID}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if source.IsOAuth2() {
 | 
						if registerableSource, ok := source.Cfg.(RegisterableSource); ok {
 | 
				
			||||||
		oauth2.RemoveProvider(source.Name)
 | 
							if err := registerableSource.UnregisterSource(); err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	_, err = x.ID(source.ID).Delete(new(LoginSource))
 | 
						_, err = x.ID(source.ID).Delete(new(LoginSource))
 | 
				
			||||||
@@ -490,404 +366,3 @@ func CountLoginSources() int64 {
 | 
				
			|||||||
	count, _ := x.Count(new(LoginSource))
 | 
						count, _ := x.Count(new(LoginSource))
 | 
				
			||||||
	return count
 | 
						return count
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
// .____     ________      _____ __________
 | 
					 | 
				
			||||||
// |    |    \______ \    /  _  \\______   \
 | 
					 | 
				
			||||||
// |    |     |    |  \  /  /_\  \|     ___/
 | 
					 | 
				
			||||||
// |    |___  |    `   \/    |    \    |
 | 
					 | 
				
			||||||
// |_______ \/_______  /\____|__  /____|
 | 
					 | 
				
			||||||
//         \/        \/         \/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func composeFullName(firstname, surname, username string) string {
 | 
					 | 
				
			||||||
	switch {
 | 
					 | 
				
			||||||
	case len(firstname) == 0 && len(surname) == 0:
 | 
					 | 
				
			||||||
		return username
 | 
					 | 
				
			||||||
	case len(firstname) == 0:
 | 
					 | 
				
			||||||
		return surname
 | 
					 | 
				
			||||||
	case len(surname) == 0:
 | 
					 | 
				
			||||||
		return firstname
 | 
					 | 
				
			||||||
	default:
 | 
					 | 
				
			||||||
		return firstname + " " + surname
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// LoginViaLDAP queries if login/password is valid against the LDAP directory pool,
 | 
					 | 
				
			||||||
// and create a local user if success when enabled.
 | 
					 | 
				
			||||||
func LoginViaLDAP(user *User, login, password string, source *LoginSource) (*User, error) {
 | 
					 | 
				
			||||||
	sr := source.Cfg.(*LDAPConfig).SearchEntry(login, password, source.Type == LoginDLDAP)
 | 
					 | 
				
			||||||
	if sr == nil {
 | 
					 | 
				
			||||||
		// User not in LDAP, do nothing
 | 
					 | 
				
			||||||
		return nil, ErrUserNotExist{0, login, 0}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	isAttributeSSHPublicKeySet := len(strings.TrimSpace(source.LDAP().AttributeSSHPublicKey)) > 0
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Update User admin flag if exist
 | 
					 | 
				
			||||||
	if isExist, err := IsUserExist(0, sr.Username); err != nil {
 | 
					 | 
				
			||||||
		return nil, err
 | 
					 | 
				
			||||||
	} else if isExist {
 | 
					 | 
				
			||||||
		if user == nil {
 | 
					 | 
				
			||||||
			user, err = GetUserByName(sr.Username)
 | 
					 | 
				
			||||||
			if err != nil {
 | 
					 | 
				
			||||||
				return nil, err
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		if user != nil && !user.ProhibitLogin {
 | 
					 | 
				
			||||||
			cols := make([]string, 0)
 | 
					 | 
				
			||||||
			if len(source.LDAP().AdminFilter) > 0 && user.IsAdmin != sr.IsAdmin {
 | 
					 | 
				
			||||||
				// Change existing admin flag only if AdminFilter option is set
 | 
					 | 
				
			||||||
				user.IsAdmin = sr.IsAdmin
 | 
					 | 
				
			||||||
				cols = append(cols, "is_admin")
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			if !user.IsAdmin && len(source.LDAP().RestrictedFilter) > 0 && user.IsRestricted != sr.IsRestricted {
 | 
					 | 
				
			||||||
				// Change existing restricted flag only if RestrictedFilter option is set
 | 
					 | 
				
			||||||
				user.IsRestricted = sr.IsRestricted
 | 
					 | 
				
			||||||
				cols = append(cols, "is_restricted")
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			if len(cols) > 0 {
 | 
					 | 
				
			||||||
				err = UpdateUserCols(user, cols...)
 | 
					 | 
				
			||||||
				if err != nil {
 | 
					 | 
				
			||||||
					return nil, err
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if user != nil {
 | 
					 | 
				
			||||||
		if isAttributeSSHPublicKeySet && synchronizeLdapSSHPublicKeys(user, source, sr.SSHPublicKey) {
 | 
					 | 
				
			||||||
			return user, RewriteAllPublicKeys()
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		return user, nil
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Fallback.
 | 
					 | 
				
			||||||
	if len(sr.Username) == 0 {
 | 
					 | 
				
			||||||
		sr.Username = login
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if len(sr.Mail) == 0 {
 | 
					 | 
				
			||||||
		sr.Mail = fmt.Sprintf("%s@localhost", sr.Username)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	user = &User{
 | 
					 | 
				
			||||||
		LowerName:    strings.ToLower(sr.Username),
 | 
					 | 
				
			||||||
		Name:         sr.Username,
 | 
					 | 
				
			||||||
		FullName:     composeFullName(sr.Name, sr.Surname, sr.Username),
 | 
					 | 
				
			||||||
		Email:        sr.Mail,
 | 
					 | 
				
			||||||
		LoginType:    source.Type,
 | 
					 | 
				
			||||||
		LoginSource:  source.ID,
 | 
					 | 
				
			||||||
		LoginName:    login,
 | 
					 | 
				
			||||||
		IsActive:     true,
 | 
					 | 
				
			||||||
		IsAdmin:      sr.IsAdmin,
 | 
					 | 
				
			||||||
		IsRestricted: sr.IsRestricted,
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	err := CreateUser(user)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if err == nil && isAttributeSSHPublicKeySet && addLdapSSHPublicKeys(user, source, sr.SSHPublicKey) {
 | 
					 | 
				
			||||||
		err = RewriteAllPublicKeys()
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return user, err
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
//   _________   __________________________
 | 
					 | 
				
			||||||
//  /   _____/  /     \__    ___/\______   \
 | 
					 | 
				
			||||||
//  \_____  \  /  \ /  \|    |    |     ___/
 | 
					 | 
				
			||||||
//  /        \/    Y    \    |    |    |
 | 
					 | 
				
			||||||
// /_______  /\____|__  /____|    |____|
 | 
					 | 
				
			||||||
//         \/         \/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type smtpLoginAuth struct {
 | 
					 | 
				
			||||||
	username, password string
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (auth *smtpLoginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) {
 | 
					 | 
				
			||||||
	return "LOGIN", []byte(auth.username), nil
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (auth *smtpLoginAuth) Next(fromServer []byte, more bool) ([]byte, error) {
 | 
					 | 
				
			||||||
	if more {
 | 
					 | 
				
			||||||
		switch string(fromServer) {
 | 
					 | 
				
			||||||
		case "Username:":
 | 
					 | 
				
			||||||
			return []byte(auth.username), nil
 | 
					 | 
				
			||||||
		case "Password:":
 | 
					 | 
				
			||||||
			return []byte(auth.password), nil
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return nil, nil
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// SMTP authentication type names.
 | 
					 | 
				
			||||||
const (
 | 
					 | 
				
			||||||
	SMTPPlain = "PLAIN"
 | 
					 | 
				
			||||||
	SMTPLogin = "LOGIN"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// SMTPAuths contains available SMTP authentication type names.
 | 
					 | 
				
			||||||
var SMTPAuths = []string{SMTPPlain, SMTPLogin}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// SMTPAuth performs an SMTP authentication.
 | 
					 | 
				
			||||||
func SMTPAuth(a smtp.Auth, cfg *SMTPConfig) error {
 | 
					 | 
				
			||||||
	c, err := smtp.Dial(fmt.Sprintf("%s:%d", cfg.Host, cfg.Port))
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		return err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	defer c.Close()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if err = c.Hello("gogs"); err != nil {
 | 
					 | 
				
			||||||
		return err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if cfg.TLS {
 | 
					 | 
				
			||||||
		if ok, _ := c.Extension("STARTTLS"); ok {
 | 
					 | 
				
			||||||
			if err = c.StartTLS(&tls.Config{
 | 
					 | 
				
			||||||
				InsecureSkipVerify: cfg.SkipVerify,
 | 
					 | 
				
			||||||
				ServerName:         cfg.Host,
 | 
					 | 
				
			||||||
			}); err != nil {
 | 
					 | 
				
			||||||
				return err
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		} else {
 | 
					 | 
				
			||||||
			return errors.New("SMTP server unsupports TLS")
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if ok, _ := c.Extension("AUTH"); ok {
 | 
					 | 
				
			||||||
		return c.Auth(a)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return ErrUnsupportedLoginType
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// LoginViaSMTP queries if login/password is valid against the SMTP,
 | 
					 | 
				
			||||||
// and create a local user if success when enabled.
 | 
					 | 
				
			||||||
func LoginViaSMTP(user *User, login, password string, sourceID int64, cfg *SMTPConfig) (*User, error) {
 | 
					 | 
				
			||||||
	// Verify allowed domains.
 | 
					 | 
				
			||||||
	if len(cfg.AllowedDomains) > 0 {
 | 
					 | 
				
			||||||
		idx := strings.Index(login, "@")
 | 
					 | 
				
			||||||
		if idx == -1 {
 | 
					 | 
				
			||||||
			return nil, ErrUserNotExist{0, login, 0}
 | 
					 | 
				
			||||||
		} else if !util.IsStringInSlice(login[idx+1:], strings.Split(cfg.AllowedDomains, ","), true) {
 | 
					 | 
				
			||||||
			return nil, ErrUserNotExist{0, login, 0}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	var auth smtp.Auth
 | 
					 | 
				
			||||||
	if cfg.Auth == SMTPPlain {
 | 
					 | 
				
			||||||
		auth = smtp.PlainAuth("", login, password, cfg.Host)
 | 
					 | 
				
			||||||
	} else if cfg.Auth == SMTPLogin {
 | 
					 | 
				
			||||||
		auth = &smtpLoginAuth{login, password}
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		return nil, errors.New("Unsupported SMTP auth type")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if err := SMTPAuth(auth, cfg); err != nil {
 | 
					 | 
				
			||||||
		// Check standard error format first,
 | 
					 | 
				
			||||||
		// then fallback to worse case.
 | 
					 | 
				
			||||||
		tperr, ok := err.(*textproto.Error)
 | 
					 | 
				
			||||||
		if (ok && tperr.Code == 535) ||
 | 
					 | 
				
			||||||
			strings.Contains(err.Error(), "Username and Password not accepted") {
 | 
					 | 
				
			||||||
			return nil, ErrUserNotExist{0, login, 0}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		return nil, err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if user != nil {
 | 
					 | 
				
			||||||
		return user, nil
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	username := login
 | 
					 | 
				
			||||||
	idx := strings.Index(login, "@")
 | 
					 | 
				
			||||||
	if idx > -1 {
 | 
					 | 
				
			||||||
		username = login[:idx]
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	user = &User{
 | 
					 | 
				
			||||||
		LowerName:   strings.ToLower(username),
 | 
					 | 
				
			||||||
		Name:        strings.ToLower(username),
 | 
					 | 
				
			||||||
		Email:       login,
 | 
					 | 
				
			||||||
		Passwd:      password,
 | 
					 | 
				
			||||||
		LoginType:   LoginSMTP,
 | 
					 | 
				
			||||||
		LoginSource: sourceID,
 | 
					 | 
				
			||||||
		LoginName:   login,
 | 
					 | 
				
			||||||
		IsActive:    true,
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return user, CreateUser(user)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// __________  _____      _____
 | 
					 | 
				
			||||||
// \______   \/  _  \    /     \
 | 
					 | 
				
			||||||
//  |     ___/  /_\  \  /  \ /  \
 | 
					 | 
				
			||||||
//  |    |  /    |    \/    Y    \
 | 
					 | 
				
			||||||
//  |____|  \____|__  /\____|__  /
 | 
					 | 
				
			||||||
//                  \/         \/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// LoginViaPAM queries if login/password is valid against the PAM,
 | 
					 | 
				
			||||||
// and create a local user if success when enabled.
 | 
					 | 
				
			||||||
func LoginViaPAM(user *User, login, password string, sourceID int64, cfg *PAMConfig) (*User, error) {
 | 
					 | 
				
			||||||
	pamLogin, err := pam.Auth(cfg.ServiceName, login, password)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		if strings.Contains(err.Error(), "Authentication failure") {
 | 
					 | 
				
			||||||
			return nil, ErrUserNotExist{0, login, 0}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		return nil, err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if user != nil {
 | 
					 | 
				
			||||||
		return user, nil
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Allow PAM sources with `@` in their name, like from Active Directory
 | 
					 | 
				
			||||||
	username := pamLogin
 | 
					 | 
				
			||||||
	email := pamLogin
 | 
					 | 
				
			||||||
	idx := strings.Index(pamLogin, "@")
 | 
					 | 
				
			||||||
	if idx > -1 {
 | 
					 | 
				
			||||||
		username = pamLogin[:idx]
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	if ValidateEmail(email) != nil {
 | 
					 | 
				
			||||||
		if cfg.EmailDomain != "" {
 | 
					 | 
				
			||||||
			email = fmt.Sprintf("%s@%s", username, cfg.EmailDomain)
 | 
					 | 
				
			||||||
		} else {
 | 
					 | 
				
			||||||
			email = fmt.Sprintf("%s@%s", username, setting.Service.NoReplyAddress)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		if ValidateEmail(email) != nil {
 | 
					 | 
				
			||||||
			email = gouuid.New().String() + "@localhost"
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	user = &User{
 | 
					 | 
				
			||||||
		LowerName:   strings.ToLower(username),
 | 
					 | 
				
			||||||
		Name:        username,
 | 
					 | 
				
			||||||
		Email:       email,
 | 
					 | 
				
			||||||
		Passwd:      password,
 | 
					 | 
				
			||||||
		LoginType:   LoginPAM,
 | 
					 | 
				
			||||||
		LoginSource: sourceID,
 | 
					 | 
				
			||||||
		LoginName:   login, // This is what the user typed in
 | 
					 | 
				
			||||||
		IsActive:    true,
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return user, CreateUser(user)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// ExternalUserLogin attempts a login using external source types.
 | 
					 | 
				
			||||||
func ExternalUserLogin(user *User, login, password string, source *LoginSource) (*User, error) {
 | 
					 | 
				
			||||||
	if !source.IsActived {
 | 
					 | 
				
			||||||
		return nil, ErrLoginSourceNotActived
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	var err error
 | 
					 | 
				
			||||||
	switch source.Type {
 | 
					 | 
				
			||||||
	case LoginLDAP, LoginDLDAP:
 | 
					 | 
				
			||||||
		user, err = LoginViaLDAP(user, login, password, source)
 | 
					 | 
				
			||||||
	case LoginSMTP:
 | 
					 | 
				
			||||||
		user, err = LoginViaSMTP(user, login, password, source.ID, source.Cfg.(*SMTPConfig))
 | 
					 | 
				
			||||||
	case LoginPAM:
 | 
					 | 
				
			||||||
		user, err = LoginViaPAM(user, login, password, source.ID, source.Cfg.(*PAMConfig))
 | 
					 | 
				
			||||||
	default:
 | 
					 | 
				
			||||||
		return nil, ErrUnsupportedLoginType
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		return nil, err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// WARN: DON'T check user.IsActive, that will be checked on reqSign so that
 | 
					 | 
				
			||||||
	// user could be hint to resend confirm email.
 | 
					 | 
				
			||||||
	if user.ProhibitLogin {
 | 
					 | 
				
			||||||
		return nil, ErrUserProhibitLogin{user.ID, user.Name}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return user, nil
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// UserSignIn validates user name and password.
 | 
					 | 
				
			||||||
func UserSignIn(username, password string) (*User, error) {
 | 
					 | 
				
			||||||
	var user *User
 | 
					 | 
				
			||||||
	if strings.Contains(username, "@") {
 | 
					 | 
				
			||||||
		user = &User{Email: strings.ToLower(strings.TrimSpace(username))}
 | 
					 | 
				
			||||||
		// check same email
 | 
					 | 
				
			||||||
		cnt, err := x.Count(user)
 | 
					 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			return nil, err
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		if cnt > 1 {
 | 
					 | 
				
			||||||
			return nil, ErrEmailAlreadyUsed{
 | 
					 | 
				
			||||||
				Email: user.Email,
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		trimmedUsername := strings.TrimSpace(username)
 | 
					 | 
				
			||||||
		if len(trimmedUsername) == 0 {
 | 
					 | 
				
			||||||
			return nil, ErrUserNotExist{0, username, 0}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		user = &User{LowerName: strings.ToLower(trimmedUsername)}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	hasUser, err := x.Get(user)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		return nil, err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if hasUser {
 | 
					 | 
				
			||||||
		switch user.LoginType {
 | 
					 | 
				
			||||||
		case LoginNoType, LoginPlain, LoginOAuth2:
 | 
					 | 
				
			||||||
			if user.IsPasswordSet() && user.ValidatePassword(password) {
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				// Update password hash if server password hash algorithm have changed
 | 
					 | 
				
			||||||
				if user.PasswdHashAlgo != setting.PasswordHashAlgo {
 | 
					 | 
				
			||||||
					if err = user.SetPassword(password); err != nil {
 | 
					 | 
				
			||||||
						return nil, err
 | 
					 | 
				
			||||||
					}
 | 
					 | 
				
			||||||
					if err = UpdateUserCols(user, "passwd", "passwd_hash_algo", "salt"); err != nil {
 | 
					 | 
				
			||||||
						return nil, err
 | 
					 | 
				
			||||||
					}
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				// WARN: DON'T check user.IsActive, that will be checked on reqSign so that
 | 
					 | 
				
			||||||
				// user could be hint to resend confirm email.
 | 
					 | 
				
			||||||
				if user.ProhibitLogin {
 | 
					 | 
				
			||||||
					return nil, ErrUserProhibitLogin{user.ID, user.Name}
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				return user, nil
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			return nil, ErrUserNotExist{user.ID, user.Name, 0}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		default:
 | 
					 | 
				
			||||||
			var source LoginSource
 | 
					 | 
				
			||||||
			hasSource, err := x.ID(user.LoginSource).Get(&source)
 | 
					 | 
				
			||||||
			if err != nil {
 | 
					 | 
				
			||||||
				return nil, err
 | 
					 | 
				
			||||||
			} else if !hasSource {
 | 
					 | 
				
			||||||
				return nil, ErrLoginSourceNotExist{user.LoginSource}
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			return ExternalUserLogin(user, user.LoginName, password, &source)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	sources := make([]*LoginSource, 0, 5)
 | 
					 | 
				
			||||||
	if err = x.Where("is_actived = ?", true).Find(&sources); err != nil {
 | 
					 | 
				
			||||||
		return nil, err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	for _, source := range sources {
 | 
					 | 
				
			||||||
		if source.IsOAuth2() || source.IsSSPI() {
 | 
					 | 
				
			||||||
			// don't try to authenticate against OAuth2 and SSPI sources here
 | 
					 | 
				
			||||||
			continue
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		authUser, err := ExternalUserLogin(nil, username, password, source)
 | 
					 | 
				
			||||||
		if err == nil {
 | 
					 | 
				
			||||||
			return authUser, nil
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if IsErrUserNotExist(err) {
 | 
					 | 
				
			||||||
			log.Debug("Failed to login '%s' via '%s': %v", username, source.Name, err)
 | 
					 | 
				
			||||||
		} else {
 | 
					 | 
				
			||||||
			log.Warn("Failed to login '%s' via '%s': %v", username, source.Name, err)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return nil, ErrUserNotExist{user.ID, user.Name, 0}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -0,0 +1,48 @@
 | 
				
			|||||||
 | 
					# type LoginSource struct {
 | 
				
			||||||
 | 
					#   ID        int64 `xorm:"pk autoincr"`
 | 
				
			||||||
 | 
					#   Type      int
 | 
				
			||||||
 | 
					#   Cfg       []byte `xorm:"TEXT"`
 | 
				
			||||||
 | 
					#   Expected  []byte `xorm:"TEXT"`
 | 
				
			||||||
 | 
					# }
 | 
				
			||||||
 | 
					-
 | 
				
			||||||
 | 
					  id: 1
 | 
				
			||||||
 | 
					  type: 1
 | 
				
			||||||
 | 
					  is_actived: false
 | 
				
			||||||
 | 
					  cfg: "{\"Source\":{\"A\":\"string\",\"B\":1}}"
 | 
				
			||||||
 | 
					  expected: "{\"Source\":{\"A\":\"string\",\"B\":1}}"
 | 
				
			||||||
 | 
					-
 | 
				
			||||||
 | 
					  id: 2
 | 
				
			||||||
 | 
					  type: 2
 | 
				
			||||||
 | 
					  is_actived: true
 | 
				
			||||||
 | 
					  cfg: "{\"Source\":{\"A\":\"string2\",\"B\":2}}"
 | 
				
			||||||
 | 
					  expected: "{\"A\":\"string2\",\"B\":2}"
 | 
				
			||||||
 | 
					-
 | 
				
			||||||
 | 
					  id: 3
 | 
				
			||||||
 | 
					  type: 3
 | 
				
			||||||
 | 
					  is_actived: false
 | 
				
			||||||
 | 
					  cfg: "{\"Source\":{\"A\":\"string3\",\"B\":3}}"
 | 
				
			||||||
 | 
					  expected: "{\"Source\":{\"A\":\"string3\",\"B\":3}}"
 | 
				
			||||||
 | 
					-
 | 
				
			||||||
 | 
					  id: 4
 | 
				
			||||||
 | 
					  type: 4
 | 
				
			||||||
 | 
					  is_actived: true
 | 
				
			||||||
 | 
					  cfg: "{\"Source\":{\"A\":\"string4\",\"B\":4}}"
 | 
				
			||||||
 | 
					  expected: "{\"Source\":{\"A\":\"string4\",\"B\":4}}"
 | 
				
			||||||
 | 
					-
 | 
				
			||||||
 | 
					  id: 5
 | 
				
			||||||
 | 
					  type: 5
 | 
				
			||||||
 | 
					  is_actived: false
 | 
				
			||||||
 | 
					  cfg: "{\"Source\":{\"A\":\"string5\",\"B\":5}}"
 | 
				
			||||||
 | 
					  expected: "{\"A\":\"string5\",\"B\":5}"
 | 
				
			||||||
 | 
					-
 | 
				
			||||||
 | 
					  id: 6
 | 
				
			||||||
 | 
					  type: 2
 | 
				
			||||||
 | 
					  is_actived: true
 | 
				
			||||||
 | 
					  cfg: "{\"A\":\"string6\",\"B\":6}"
 | 
				
			||||||
 | 
					  expected: "{\"A\":\"string6\",\"B\":6}"
 | 
				
			||||||
 | 
					-
 | 
				
			||||||
 | 
					  id: 7
 | 
				
			||||||
 | 
					  type: 5
 | 
				
			||||||
 | 
					  is_actived: false
 | 
				
			||||||
 | 
					  cfg: "{\"A\":\"string7\",\"B\":7}"
 | 
				
			||||||
 | 
					  expected: "{\"A\":\"string7\",\"B\":7}"
 | 
				
			||||||
@@ -327,6 +327,8 @@ var migrations = []Migration{
 | 
				
			|||||||
	NewMigration("Drop unneeded webhook related columns", dropWebhookColumns),
 | 
						NewMigration("Drop unneeded webhook related columns", dropWebhookColumns),
 | 
				
			||||||
	// v188 -> v189
 | 
						// v188 -> v189
 | 
				
			||||||
	NewMigration("Add key is verified to gpg key", addKeyIsVerified),
 | 
						NewMigration("Add key is verified to gpg key", addKeyIsVerified),
 | 
				
			||||||
 | 
						// v189 -> v190
 | 
				
			||||||
 | 
						NewMigration("Unwrap ldap.Sources", unwrapLDAPSourceCfg),
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// GetCurrentDBVersion returns the current db version
 | 
					// GetCurrentDBVersion returns the current db version
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -220,6 +220,9 @@ func prepareTestEnv(t *testing.T, skip int, syncModels ...interface{}) (*xorm.En
 | 
				
			|||||||
			if err := x.Close(); err != nil {
 | 
								if err := x.Close(); err != nil {
 | 
				
			||||||
				t.Errorf("error during close: %v", err)
 | 
									t.Errorf("error during close: %v", err)
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
								if err := deleteDB(); err != nil {
 | 
				
			||||||
 | 
									t.Errorf("unable to reset database: %v", err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										111
									
								
								models/migrations/v189.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										111
									
								
								models/migrations/v189.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,111 @@
 | 
				
			|||||||
 | 
					// Copyright 2021 The Gitea Authors. All rights reserved.
 | 
				
			||||||
 | 
					// Use of this source code is governed by a MIT-style
 | 
				
			||||||
 | 
					// license that can be found in the LICENSE file.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package migrations
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"encoding/binary"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						jsoniter "github.com/json-iterator/go"
 | 
				
			||||||
 | 
						"xorm.io/xorm"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func unwrapLDAPSourceCfg(x *xorm.Engine) error {
 | 
				
			||||||
 | 
						jsonUnmarshalHandleDoubleEncode := func(bs []byte, v interface{}) error {
 | 
				
			||||||
 | 
							json := jsoniter.ConfigCompatibleWithStandardLibrary
 | 
				
			||||||
 | 
							err := json.Unmarshal(bs, v)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								ok := true
 | 
				
			||||||
 | 
								rs := []byte{}
 | 
				
			||||||
 | 
								temp := make([]byte, 2)
 | 
				
			||||||
 | 
								for _, rn := range string(bs) {
 | 
				
			||||||
 | 
									if rn > 0xffff {
 | 
				
			||||||
 | 
										ok = false
 | 
				
			||||||
 | 
										break
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									binary.LittleEndian.PutUint16(temp, uint16(rn))
 | 
				
			||||||
 | 
									rs = append(rs, temp...)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if ok {
 | 
				
			||||||
 | 
									if rs[0] == 0xff && rs[1] == 0xfe {
 | 
				
			||||||
 | 
										rs = rs[2:]
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									err = json.Unmarshal(rs, v)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if err != nil && len(bs) > 2 && bs[0] == 0xff && bs[1] == 0xfe {
 | 
				
			||||||
 | 
								err = json.Unmarshal(bs[2:], v)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// LoginSource represents an external way for authorizing users.
 | 
				
			||||||
 | 
						type LoginSource struct {
 | 
				
			||||||
 | 
							ID        int64 `xorm:"pk autoincr"`
 | 
				
			||||||
 | 
							Type      int
 | 
				
			||||||
 | 
							IsActived bool   `xorm:"INDEX NOT NULL DEFAULT false"`
 | 
				
			||||||
 | 
							IsActive  bool   `xorm:"INDEX NOT NULL DEFAULT false"`
 | 
				
			||||||
 | 
							Cfg       string `xorm:"TEXT"`
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const ldapType = 2
 | 
				
			||||||
 | 
						const dldapType = 5
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						type WrappedSource struct {
 | 
				
			||||||
 | 
							Source map[string]interface{}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// change lower_email as unique
 | 
				
			||||||
 | 
						if err := x.Sync2(new(LoginSource)); err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						sess := x.NewSession()
 | 
				
			||||||
 | 
						defer sess.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const batchSize = 100
 | 
				
			||||||
 | 
						for start := 0; ; start += batchSize {
 | 
				
			||||||
 | 
							sources := make([]*LoginSource, 0, batchSize)
 | 
				
			||||||
 | 
							if err := sess.Limit(batchSize, start).Where("`type` = ? OR `type` = ?", ldapType, dldapType).Find(&sources); err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if len(sources) == 0 {
 | 
				
			||||||
 | 
								break
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							for _, source := range sources {
 | 
				
			||||||
 | 
								wrapped := &WrappedSource{
 | 
				
			||||||
 | 
									Source: map[string]interface{}{},
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								err := jsonUnmarshalHandleDoubleEncode([]byte(source.Cfg), &wrapped)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									return fmt.Errorf("failed to unmarshal %s: %w", string(source.Cfg), err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if wrapped.Source != nil && len(wrapped.Source) > 0 {
 | 
				
			||||||
 | 
									bs, err := jsoniter.Marshal(wrapped.Source)
 | 
				
			||||||
 | 
									if err != nil {
 | 
				
			||||||
 | 
										return err
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									source.Cfg = string(bs)
 | 
				
			||||||
 | 
									if _, err := sess.ID(source.ID).Cols("cfg").Update(source); err != nil {
 | 
				
			||||||
 | 
										return err
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if _, err := x.SetExpr("is_active", "is_actived").Update(&LoginSource{}); err != nil {
 | 
				
			||||||
 | 
							return fmt.Errorf("SetExpr Update failed:  %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := sess.Begin(); err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if err := dropTableColumns(sess, "login_source", "is_actived"); err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return sess.Commit()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										83
									
								
								models/migrations/v189_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								models/migrations/v189_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,83 @@
 | 
				
			|||||||
 | 
					// Copyright 2021 The Gitea Authors. All rights reserved.
 | 
				
			||||||
 | 
					// Use of this source code is governed by a MIT-style
 | 
				
			||||||
 | 
					// license that can be found in the LICENSE file.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package migrations
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						jsoniter "github.com/json-iterator/go"
 | 
				
			||||||
 | 
						"github.com/stretchr/testify/assert"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// LoginSource represents an external way for authorizing users.
 | 
				
			||||||
 | 
					type LoginSourceOriginalV189 struct {
 | 
				
			||||||
 | 
						ID        int64 `xorm:"pk autoincr"`
 | 
				
			||||||
 | 
						Type      int
 | 
				
			||||||
 | 
						IsActived bool   `xorm:"INDEX NOT NULL DEFAULT false"`
 | 
				
			||||||
 | 
						Cfg       string `xorm:"TEXT"`
 | 
				
			||||||
 | 
						Expected  string `xorm:"TEXT"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (ls *LoginSourceOriginalV189) TableName() string {
 | 
				
			||||||
 | 
						return "login_source"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func Test_unwrapLDAPSourceCfg(t *testing.T) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Prepare and load the testing database
 | 
				
			||||||
 | 
						x, deferable := prepareTestEnv(t, 0, new(LoginSourceOriginalV189))
 | 
				
			||||||
 | 
						if x == nil || t.Failed() {
 | 
				
			||||||
 | 
							defer deferable()
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						defer deferable()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// LoginSource represents an external way for authorizing users.
 | 
				
			||||||
 | 
						type LoginSource struct {
 | 
				
			||||||
 | 
							ID       int64 `xorm:"pk autoincr"`
 | 
				
			||||||
 | 
							Type     int
 | 
				
			||||||
 | 
							IsActive bool   `xorm:"INDEX NOT NULL DEFAULT false"`
 | 
				
			||||||
 | 
							Cfg      string `xorm:"TEXT"`
 | 
				
			||||||
 | 
							Expected string `xorm:"TEXT"`
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Run the migration
 | 
				
			||||||
 | 
						if err := unwrapLDAPSourceCfg(x); err != nil {
 | 
				
			||||||
 | 
							assert.NoError(t, err)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const batchSize = 100
 | 
				
			||||||
 | 
						for start := 0; ; start += batchSize {
 | 
				
			||||||
 | 
							sources := make([]*LoginSource, 0, batchSize)
 | 
				
			||||||
 | 
							if err := x.Table("login_source").Limit(batchSize, start).Find(&sources); err != nil {
 | 
				
			||||||
 | 
								assert.NoError(t, err)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if len(sources) == 0 {
 | 
				
			||||||
 | 
								break
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							for _, source := range sources {
 | 
				
			||||||
 | 
								converted := map[string]interface{}{}
 | 
				
			||||||
 | 
								expected := map[string]interface{}{}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if err := jsoniter.Unmarshal([]byte(source.Cfg), &converted); err != nil {
 | 
				
			||||||
 | 
									assert.NoError(t, err)
 | 
				
			||||||
 | 
									return
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if err := jsoniter.Unmarshal([]byte(source.Expected), &expected); err != nil {
 | 
				
			||||||
 | 
									assert.NoError(t, err)
 | 
				
			||||||
 | 
									return
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								assert.EqualValues(t, expected, converted, "unwrapLDAPSourceCfg failed for %d", source.ID)
 | 
				
			||||||
 | 
								assert.EqualValues(t, source.ID%2 == 0, source.IsActive, "unwrapLDAPSourceCfg failed for %d", source.ID)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										154
									
								
								models/oauth2.go
									
									
									
									
									
								
							
							
						
						
									
										154
									
								
								models/oauth2.go
									
									
									
									
									
								
							@@ -4,89 +4,10 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
package models
 | 
					package models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"sort"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	"code.gitea.io/gitea/modules/auth/oauth2"
 | 
					 | 
				
			||||||
	"code.gitea.io/gitea/modules/log"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// OAuth2Provider describes the display values of a single OAuth2 provider
 | 
					 | 
				
			||||||
type OAuth2Provider struct {
 | 
					 | 
				
			||||||
	Name             string
 | 
					 | 
				
			||||||
	DisplayName      string
 | 
					 | 
				
			||||||
	Image            string
 | 
					 | 
				
			||||||
	CustomURLMapping *oauth2.CustomURLMapping
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// OAuth2Providers contains the map of registered OAuth2 providers in Gitea (based on goth)
 | 
					 | 
				
			||||||
// key is used to map the OAuth2Provider with the goth provider type (also in LoginSource.OAuth2Config.Provider)
 | 
					 | 
				
			||||||
// value is used to store display data
 | 
					 | 
				
			||||||
var OAuth2Providers = map[string]OAuth2Provider{
 | 
					 | 
				
			||||||
	"bitbucket": {Name: "bitbucket", DisplayName: "Bitbucket", Image: "/assets/img/auth/bitbucket.png"},
 | 
					 | 
				
			||||||
	"dropbox":   {Name: "dropbox", DisplayName: "Dropbox", Image: "/assets/img/auth/dropbox.png"},
 | 
					 | 
				
			||||||
	"facebook":  {Name: "facebook", DisplayName: "Facebook", Image: "/assets/img/auth/facebook.png"},
 | 
					 | 
				
			||||||
	"github": {
 | 
					 | 
				
			||||||
		Name: "github", DisplayName: "GitHub", Image: "/assets/img/auth/github.png",
 | 
					 | 
				
			||||||
		CustomURLMapping: &oauth2.CustomURLMapping{
 | 
					 | 
				
			||||||
			TokenURL:   oauth2.GetDefaultTokenURL("github"),
 | 
					 | 
				
			||||||
			AuthURL:    oauth2.GetDefaultAuthURL("github"),
 | 
					 | 
				
			||||||
			ProfileURL: oauth2.GetDefaultProfileURL("github"),
 | 
					 | 
				
			||||||
			EmailURL:   oauth2.GetDefaultEmailURL("github"),
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
	"gitlab": {
 | 
					 | 
				
			||||||
		Name: "gitlab", DisplayName: "GitLab", Image: "/assets/img/auth/gitlab.png",
 | 
					 | 
				
			||||||
		CustomURLMapping: &oauth2.CustomURLMapping{
 | 
					 | 
				
			||||||
			TokenURL:   oauth2.GetDefaultTokenURL("gitlab"),
 | 
					 | 
				
			||||||
			AuthURL:    oauth2.GetDefaultAuthURL("gitlab"),
 | 
					 | 
				
			||||||
			ProfileURL: oauth2.GetDefaultProfileURL("gitlab"),
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
	"gplus":         {Name: "gplus", DisplayName: "Google", Image: "/assets/img/auth/google.png"},
 | 
					 | 
				
			||||||
	"openidConnect": {Name: "openidConnect", DisplayName: "OpenID Connect", Image: "/assets/img/auth/openid_connect.svg"},
 | 
					 | 
				
			||||||
	"twitter":       {Name: "twitter", DisplayName: "Twitter", Image: "/assets/img/auth/twitter.png"},
 | 
					 | 
				
			||||||
	"discord":       {Name: "discord", DisplayName: "Discord", Image: "/assets/img/auth/discord.png"},
 | 
					 | 
				
			||||||
	"gitea": {
 | 
					 | 
				
			||||||
		Name: "gitea", DisplayName: "Gitea", Image: "/assets/img/auth/gitea.png",
 | 
					 | 
				
			||||||
		CustomURLMapping: &oauth2.CustomURLMapping{
 | 
					 | 
				
			||||||
			TokenURL:   oauth2.GetDefaultTokenURL("gitea"),
 | 
					 | 
				
			||||||
			AuthURL:    oauth2.GetDefaultAuthURL("gitea"),
 | 
					 | 
				
			||||||
			ProfileURL: oauth2.GetDefaultProfileURL("gitea"),
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
	"nextcloud": {
 | 
					 | 
				
			||||||
		Name: "nextcloud", DisplayName: "Nextcloud", Image: "/assets/img/auth/nextcloud.png",
 | 
					 | 
				
			||||||
		CustomURLMapping: &oauth2.CustomURLMapping{
 | 
					 | 
				
			||||||
			TokenURL:   oauth2.GetDefaultTokenURL("nextcloud"),
 | 
					 | 
				
			||||||
			AuthURL:    oauth2.GetDefaultAuthURL("nextcloud"),
 | 
					 | 
				
			||||||
			ProfileURL: oauth2.GetDefaultProfileURL("nextcloud"),
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
	"yandex": {Name: "yandex", DisplayName: "Yandex", Image: "/assets/img/auth/yandex.png"},
 | 
					 | 
				
			||||||
	"mastodon": {
 | 
					 | 
				
			||||||
		Name: "mastodon", DisplayName: "Mastodon", Image: "/assets/img/auth/mastodon.png",
 | 
					 | 
				
			||||||
		CustomURLMapping: &oauth2.CustomURLMapping{
 | 
					 | 
				
			||||||
			AuthURL: oauth2.GetDefaultAuthURL("mastodon"),
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// OAuth2DefaultCustomURLMappings contains the map of default URL's for OAuth2 providers that are allowed to have custom urls
 | 
					 | 
				
			||||||
// key is used to map the OAuth2Provider
 | 
					 | 
				
			||||||
// value is the mapping as defined for the OAuth2Provider
 | 
					 | 
				
			||||||
var OAuth2DefaultCustomURLMappings = map[string]*oauth2.CustomURLMapping{
 | 
					 | 
				
			||||||
	"github":    OAuth2Providers["github"].CustomURLMapping,
 | 
					 | 
				
			||||||
	"gitlab":    OAuth2Providers["gitlab"].CustomURLMapping,
 | 
					 | 
				
			||||||
	"gitea":     OAuth2Providers["gitea"].CustomURLMapping,
 | 
					 | 
				
			||||||
	"nextcloud": OAuth2Providers["nextcloud"].CustomURLMapping,
 | 
					 | 
				
			||||||
	"mastodon":  OAuth2Providers["mastodon"].CustomURLMapping,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// GetActiveOAuth2ProviderLoginSources returns all actived LoginOAuth2 sources
 | 
					// GetActiveOAuth2ProviderLoginSources returns all actived LoginOAuth2 sources
 | 
				
			||||||
func GetActiveOAuth2ProviderLoginSources() ([]*LoginSource, error) {
 | 
					func GetActiveOAuth2ProviderLoginSources() ([]*LoginSource, error) {
 | 
				
			||||||
	sources := make([]*LoginSource, 0, 1)
 | 
						sources := make([]*LoginSource, 0, 1)
 | 
				
			||||||
	if err := x.Where("is_actived = ? and type = ?", true, LoginOAuth2).Find(&sources); err != nil {
 | 
						if err := x.Where("is_active = ? and type = ?", true, LoginOAuth2).Find(&sources); err != nil {
 | 
				
			||||||
		return nil, err
 | 
							return nil, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	return sources, nil
 | 
						return sources, nil
 | 
				
			||||||
@@ -95,81 +16,10 @@ func GetActiveOAuth2ProviderLoginSources() ([]*LoginSource, error) {
 | 
				
			|||||||
// GetActiveOAuth2LoginSourceByName returns a OAuth2 LoginSource based on the given name
 | 
					// GetActiveOAuth2LoginSourceByName returns a OAuth2 LoginSource based on the given name
 | 
				
			||||||
func GetActiveOAuth2LoginSourceByName(name string) (*LoginSource, error) {
 | 
					func GetActiveOAuth2LoginSourceByName(name string) (*LoginSource, error) {
 | 
				
			||||||
	loginSource := new(LoginSource)
 | 
						loginSource := new(LoginSource)
 | 
				
			||||||
	has, err := x.Where("name = ? and type = ? and is_actived = ?", name, LoginOAuth2, true).Get(loginSource)
 | 
						has, err := x.Where("name = ? and type = ? and is_active = ?", name, LoginOAuth2, true).Get(loginSource)
 | 
				
			||||||
	if !has || err != nil {
 | 
						if !has || err != nil {
 | 
				
			||||||
		return nil, err
 | 
							return nil, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return loginSource, nil
 | 
						return loginSource, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
// GetActiveOAuth2Providers returns the map of configured active OAuth2 providers
 | 
					 | 
				
			||||||
// key is used as technical name (like in the callbackURL)
 | 
					 | 
				
			||||||
// values to display
 | 
					 | 
				
			||||||
func GetActiveOAuth2Providers() ([]string, map[string]OAuth2Provider, error) {
 | 
					 | 
				
			||||||
	// Maybe also separate used and unused providers so we can force the registration of only 1 active provider for each type
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	loginSources, err := GetActiveOAuth2ProviderLoginSources()
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		return nil, nil, err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	var orderedKeys []string
 | 
					 | 
				
			||||||
	providers := make(map[string]OAuth2Provider)
 | 
					 | 
				
			||||||
	for _, source := range loginSources {
 | 
					 | 
				
			||||||
		prov := OAuth2Providers[source.OAuth2().Provider]
 | 
					 | 
				
			||||||
		if source.OAuth2().IconURL != "" {
 | 
					 | 
				
			||||||
			prov.Image = source.OAuth2().IconURL
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		providers[source.Name] = prov
 | 
					 | 
				
			||||||
		orderedKeys = append(orderedKeys, source.Name)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	sort.Strings(orderedKeys)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return orderedKeys, providers, nil
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// InitOAuth2 initialize the OAuth2 lib and register all active OAuth2 providers in the library
 | 
					 | 
				
			||||||
func InitOAuth2() error {
 | 
					 | 
				
			||||||
	if err := oauth2.InitSigningKey(); err != nil {
 | 
					 | 
				
			||||||
		return err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	if err := oauth2.Init(x); err != nil {
 | 
					 | 
				
			||||||
		return err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return initOAuth2LoginSources()
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// ResetOAuth2 clears existing OAuth2 providers and loads them from DB
 | 
					 | 
				
			||||||
func ResetOAuth2() error {
 | 
					 | 
				
			||||||
	oauth2.ClearProviders()
 | 
					 | 
				
			||||||
	return initOAuth2LoginSources()
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// initOAuth2LoginSources is used to load and register all active OAuth2 providers
 | 
					 | 
				
			||||||
func initOAuth2LoginSources() error {
 | 
					 | 
				
			||||||
	loginSources, _ := GetActiveOAuth2ProviderLoginSources()
 | 
					 | 
				
			||||||
	for _, source := range loginSources {
 | 
					 | 
				
			||||||
		oAuth2Config := source.OAuth2()
 | 
					 | 
				
			||||||
		err := oauth2.RegisterProvider(source.Name, oAuth2Config.Provider, oAuth2Config.ClientID, oAuth2Config.ClientSecret, oAuth2Config.OpenIDConnectAutoDiscoveryURL, oAuth2Config.CustomURLMapping)
 | 
					 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			log.Critical("Unable to register source: %s due to Error: %v. This source will be disabled.", source.Name, err)
 | 
					 | 
				
			||||||
			source.IsActived = false
 | 
					 | 
				
			||||||
			if err = UpdateSource(source); err != nil {
 | 
					 | 
				
			||||||
				log.Critical("Unable to update source %s to disable it. Error: %v", err)
 | 
					 | 
				
			||||||
				return err
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return nil
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// wrapOpenIDConnectInitializeError is used to wrap the error but this cannot be done in modules/auth/oauth2
 | 
					 | 
				
			||||||
// inside oauth2: import cycle not allowed models -> modules/auth/oauth2 -> models
 | 
					 | 
				
			||||||
func wrapOpenIDConnectInitializeError(err error, providerName string, oAuth2Config *OAuth2Config) error {
 | 
					 | 
				
			||||||
	if err != nil && "openidConnect" == oAuth2Config.Provider {
 | 
					 | 
				
			||||||
		err = ErrOpenIDConnectInitialize{ProviderName: providerName, OpenIDConnectAutoDiscoveryURL: oAuth2Config.OpenIDConnectAutoDiscoveryURL, Cause: err}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return err
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,14 +10,11 @@ import (
 | 
				
			|||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"net/url"
 | 
						"net/url"
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
	"time"
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"code.gitea.io/gitea/modules/auth/oauth2"
 | 
					 | 
				
			||||||
	"code.gitea.io/gitea/modules/secret"
 | 
						"code.gitea.io/gitea/modules/secret"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/timeutil"
 | 
						"code.gitea.io/gitea/modules/timeutil"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/util"
 | 
						"code.gitea.io/gitea/modules/util"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"github.com/dgrijalva/jwt-go"
 | 
					 | 
				
			||||||
	uuid "github.com/google/uuid"
 | 
						uuid "github.com/google/uuid"
 | 
				
			||||||
	"golang.org/x/crypto/bcrypt"
 | 
						"golang.org/x/crypto/bcrypt"
 | 
				
			||||||
	"xorm.io/xorm"
 | 
						"xorm.io/xorm"
 | 
				
			||||||
@@ -516,77 +513,3 @@ func revokeOAuth2Grant(e Engine, grantID, userID int64) error {
 | 
				
			|||||||
	_, err := e.Delete(&OAuth2Grant{ID: grantID, UserID: userID})
 | 
						_, err := e.Delete(&OAuth2Grant{ID: grantID, UserID: userID})
 | 
				
			||||||
	return err
 | 
						return err
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
//////////////////////////////////////////////////////////////
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// OAuth2TokenType represents the type of token for an oauth application
 | 
					 | 
				
			||||||
type OAuth2TokenType int
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const (
 | 
					 | 
				
			||||||
	// TypeAccessToken is a token with short lifetime to access the api
 | 
					 | 
				
			||||||
	TypeAccessToken OAuth2TokenType = 0
 | 
					 | 
				
			||||||
	// TypeRefreshToken is token with long lifetime to refresh access tokens obtained by the client
 | 
					 | 
				
			||||||
	TypeRefreshToken = iota
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// OAuth2Token represents a JWT token used to authenticate a client
 | 
					 | 
				
			||||||
type OAuth2Token struct {
 | 
					 | 
				
			||||||
	GrantID int64           `json:"gnt"`
 | 
					 | 
				
			||||||
	Type    OAuth2TokenType `json:"tt"`
 | 
					 | 
				
			||||||
	Counter int64           `json:"cnt,omitempty"`
 | 
					 | 
				
			||||||
	jwt.StandardClaims
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// ParseOAuth2Token parses a signed jwt string
 | 
					 | 
				
			||||||
func ParseOAuth2Token(jwtToken string) (*OAuth2Token, error) {
 | 
					 | 
				
			||||||
	parsedToken, err := jwt.ParseWithClaims(jwtToken, &OAuth2Token{}, func(token *jwt.Token) (interface{}, error) {
 | 
					 | 
				
			||||||
		if token.Method == nil || token.Method.Alg() != oauth2.DefaultSigningKey.SigningMethod().Alg() {
 | 
					 | 
				
			||||||
			return nil, fmt.Errorf("unexpected signing algo: %v", token.Header["alg"])
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		return oauth2.DefaultSigningKey.VerifyKey(), nil
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		return nil, err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	var token *OAuth2Token
 | 
					 | 
				
			||||||
	var ok bool
 | 
					 | 
				
			||||||
	if token, ok = parsedToken.Claims.(*OAuth2Token); !ok || !parsedToken.Valid {
 | 
					 | 
				
			||||||
		return nil, fmt.Errorf("invalid token")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return token, nil
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// SignToken signs the token with the JWT secret
 | 
					 | 
				
			||||||
func (token *OAuth2Token) SignToken() (string, error) {
 | 
					 | 
				
			||||||
	token.IssuedAt = time.Now().Unix()
 | 
					 | 
				
			||||||
	jwtToken := jwt.NewWithClaims(oauth2.DefaultSigningKey.SigningMethod(), token)
 | 
					 | 
				
			||||||
	oauth2.DefaultSigningKey.PreProcessToken(jwtToken)
 | 
					 | 
				
			||||||
	return jwtToken.SignedString(oauth2.DefaultSigningKey.SignKey())
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// OIDCToken represents an OpenID Connect id_token
 | 
					 | 
				
			||||||
type OIDCToken struct {
 | 
					 | 
				
			||||||
	jwt.StandardClaims
 | 
					 | 
				
			||||||
	Nonce string `json:"nonce,omitempty"`
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Scope profile
 | 
					 | 
				
			||||||
	Name              string             `json:"name,omitempty"`
 | 
					 | 
				
			||||||
	PreferredUsername string             `json:"preferred_username,omitempty"`
 | 
					 | 
				
			||||||
	Profile           string             `json:"profile,omitempty"`
 | 
					 | 
				
			||||||
	Picture           string             `json:"picture,omitempty"`
 | 
					 | 
				
			||||||
	Website           string             `json:"website,omitempty"`
 | 
					 | 
				
			||||||
	Locale            string             `json:"locale,omitempty"`
 | 
					 | 
				
			||||||
	UpdatedAt         timeutil.TimeStamp `json:"updated_at,omitempty"`
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Scope email
 | 
					 | 
				
			||||||
	Email         string `json:"email,omitempty"`
 | 
					 | 
				
			||||||
	EmailVerified bool   `json:"email_verified,omitempty"`
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// SignToken signs an id_token with the (symmetric) client secret key
 | 
					 | 
				
			||||||
func (token *OIDCToken) SignToken(signingKey oauth2.JWTSigningKey) (string, error) {
 | 
					 | 
				
			||||||
	token.IssuedAt = time.Now().Unix()
 | 
					 | 
				
			||||||
	jwtToken := jwt.NewWithClaims(signingKey.SigningMethod(), token)
 | 
					 | 
				
			||||||
	signingKey.PreProcessToken(jwtToken)
 | 
					 | 
				
			||||||
	return jwtToken.SignedString(signingKey.SignKey())
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -28,7 +28,7 @@ type UnitConfig struct{}
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// FromDB fills up a UnitConfig from serialized format.
 | 
					// FromDB fills up a UnitConfig from serialized format.
 | 
				
			||||||
func (cfg *UnitConfig) FromDB(bs []byte) error {
 | 
					func (cfg *UnitConfig) FromDB(bs []byte) error {
 | 
				
			||||||
	return jsonUnmarshalHandleDoubleEncode(bs, &cfg)
 | 
						return JSONUnmarshalHandleDoubleEncode(bs, &cfg)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// ToDB exports a UnitConfig to a serialized format.
 | 
					// ToDB exports a UnitConfig to a serialized format.
 | 
				
			||||||
@@ -44,7 +44,7 @@ type ExternalWikiConfig struct {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// FromDB fills up a ExternalWikiConfig from serialized format.
 | 
					// FromDB fills up a ExternalWikiConfig from serialized format.
 | 
				
			||||||
func (cfg *ExternalWikiConfig) FromDB(bs []byte) error {
 | 
					func (cfg *ExternalWikiConfig) FromDB(bs []byte) error {
 | 
				
			||||||
	return jsonUnmarshalHandleDoubleEncode(bs, &cfg)
 | 
						return JSONUnmarshalHandleDoubleEncode(bs, &cfg)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// ToDB exports a ExternalWikiConfig to a serialized format.
 | 
					// ToDB exports a ExternalWikiConfig to a serialized format.
 | 
				
			||||||
@@ -62,7 +62,7 @@ type ExternalTrackerConfig struct {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// FromDB fills up a ExternalTrackerConfig from serialized format.
 | 
					// FromDB fills up a ExternalTrackerConfig from serialized format.
 | 
				
			||||||
func (cfg *ExternalTrackerConfig) FromDB(bs []byte) error {
 | 
					func (cfg *ExternalTrackerConfig) FromDB(bs []byte) error {
 | 
				
			||||||
	return jsonUnmarshalHandleDoubleEncode(bs, &cfg)
 | 
						return JSONUnmarshalHandleDoubleEncode(bs, &cfg)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// ToDB exports a ExternalTrackerConfig to a serialized format.
 | 
					// ToDB exports a ExternalTrackerConfig to a serialized format.
 | 
				
			||||||
@@ -80,7 +80,7 @@ type IssuesConfig struct {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// FromDB fills up a IssuesConfig from serialized format.
 | 
					// FromDB fills up a IssuesConfig from serialized format.
 | 
				
			||||||
func (cfg *IssuesConfig) FromDB(bs []byte) error {
 | 
					func (cfg *IssuesConfig) FromDB(bs []byte) error {
 | 
				
			||||||
	return jsonUnmarshalHandleDoubleEncode(bs, &cfg)
 | 
						return JSONUnmarshalHandleDoubleEncode(bs, &cfg)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// ToDB exports a IssuesConfig to a serialized format.
 | 
					// ToDB exports a IssuesConfig to a serialized format.
 | 
				
			||||||
@@ -104,7 +104,7 @@ type PullRequestsConfig struct {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// FromDB fills up a PullRequestsConfig from serialized format.
 | 
					// FromDB fills up a PullRequestsConfig from serialized format.
 | 
				
			||||||
func (cfg *PullRequestsConfig) FromDB(bs []byte) error {
 | 
					func (cfg *PullRequestsConfig) FromDB(bs []byte) error {
 | 
				
			||||||
	return jsonUnmarshalHandleDoubleEncode(bs, &cfg)
 | 
						return JSONUnmarshalHandleDoubleEncode(bs, &cfg)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// ToDB exports a PullRequestsConfig to a serialized format.
 | 
					// ToDB exports a PullRequestsConfig to a serialized format.
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										1142
									
								
								models/ssh_key.go
									
									
									
									
									
								
							
							
						
						
									
										1142
									
								
								models/ssh_key.go
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										219
									
								
								models/ssh_key_authorized_keys.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										219
									
								
								models/ssh_key_authorized_keys.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,219 @@
 | 
				
			|||||||
 | 
					// Copyright 2021 The Gitea Authors. All rights reserved.
 | 
				
			||||||
 | 
					// Use of this source code is governed by a MIT-style
 | 
				
			||||||
 | 
					// license that can be found in the LICENSE file.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"bufio"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"io"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"path/filepath"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
						"sync"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/log"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/setting"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/util"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//  _____          __  .__                 .__                  .___
 | 
				
			||||||
 | 
					// /  _  \  __ ___/  |_|  |__   ___________|__|_______ ____   __| _/
 | 
				
			||||||
 | 
					// /  /_\  \|  |  \   __\  |  \ /  _ \_  __ \  \___   // __ \ / __ |
 | 
				
			||||||
 | 
					// /    |    \  |  /|  | |   Y  (  <_> )  | \/  |/    /\  ___// /_/ |
 | 
				
			||||||
 | 
					// \____|__  /____/ |__| |___|  /\____/|__|  |__/_____ \\___  >____ |
 | 
				
			||||||
 | 
					//         \/                 \/                      \/    \/     \/
 | 
				
			||||||
 | 
					// ____  __.
 | 
				
			||||||
 | 
					// |    |/ _|____ ___.__. ______
 | 
				
			||||||
 | 
					// |      <_/ __ <   |  |/  ___/
 | 
				
			||||||
 | 
					// |    |  \  ___/\___  |\___ \
 | 
				
			||||||
 | 
					// |____|__ \___  > ____/____  >
 | 
				
			||||||
 | 
					//         \/   \/\/         \/
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// This file contains functions for creating authorized_keys files
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// There is a dependence on the database within RegeneratePublicKeys however most of these functions probably belong in a module
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						tplCommentPrefix = `# gitea public key`
 | 
				
			||||||
 | 
						tplPublicKey     = tplCommentPrefix + "\n" + `command=%s,no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty %s` + "\n"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var sshOpLocker sync.Mutex
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// AuthorizedStringForKey creates the authorized keys string appropriate for the provided key
 | 
				
			||||||
 | 
					func AuthorizedStringForKey(key *PublicKey) string {
 | 
				
			||||||
 | 
						sb := &strings.Builder{}
 | 
				
			||||||
 | 
						_ = setting.SSH.AuthorizedKeysCommandTemplateTemplate.Execute(sb, map[string]interface{}{
 | 
				
			||||||
 | 
							"AppPath":     util.ShellEscape(setting.AppPath),
 | 
				
			||||||
 | 
							"AppWorkPath": util.ShellEscape(setting.AppWorkPath),
 | 
				
			||||||
 | 
							"CustomConf":  util.ShellEscape(setting.CustomConf),
 | 
				
			||||||
 | 
							"CustomPath":  util.ShellEscape(setting.CustomPath),
 | 
				
			||||||
 | 
							"Key":         key,
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return fmt.Sprintf(tplPublicKey, util.ShellEscape(sb.String()), key.Content)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// appendAuthorizedKeysToFile appends new SSH keys' content to authorized_keys file.
 | 
				
			||||||
 | 
					func appendAuthorizedKeysToFile(keys ...*PublicKey) error {
 | 
				
			||||||
 | 
						// Don't need to rewrite this file if builtin SSH server is enabled.
 | 
				
			||||||
 | 
						if setting.SSH.StartBuiltinServer || !setting.SSH.CreateAuthorizedKeysFile {
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						sshOpLocker.Lock()
 | 
				
			||||||
 | 
						defer sshOpLocker.Unlock()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if setting.SSH.RootPath != "" {
 | 
				
			||||||
 | 
							// First of ensure that the RootPath is present, and if not make it with 0700 permissions
 | 
				
			||||||
 | 
							// This of course doesn't guarantee that this is the right directory for authorized_keys
 | 
				
			||||||
 | 
							// but at least if it's supposed to be this directory and it doesn't exist and we're the
 | 
				
			||||||
 | 
							// right user it will at least be created properly.
 | 
				
			||||||
 | 
							err := os.MkdirAll(setting.SSH.RootPath, 0o700)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								log.Error("Unable to MkdirAll(%s): %v", setting.SSH.RootPath, err)
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						fPath := filepath.Join(setting.SSH.RootPath, "authorized_keys")
 | 
				
			||||||
 | 
						f, err := os.OpenFile(fPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o600)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						defer f.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Note: chmod command does not support in Windows.
 | 
				
			||||||
 | 
						if !setting.IsWindows {
 | 
				
			||||||
 | 
							fi, err := f.Stat()
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// .ssh directory should have mode 700, and authorized_keys file should have mode 600.
 | 
				
			||||||
 | 
							if fi.Mode().Perm() > 0o600 {
 | 
				
			||||||
 | 
								log.Error("authorized_keys file has unusual permission flags: %s - setting to -rw-------", fi.Mode().Perm().String())
 | 
				
			||||||
 | 
								if err = f.Chmod(0o600); err != nil {
 | 
				
			||||||
 | 
									return err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, key := range keys {
 | 
				
			||||||
 | 
							if key.Type == KeyTypePrincipal {
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if _, err = f.WriteString(key.AuthorizedString()); err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// RewriteAllPublicKeys removes any authorized key and rewrite all keys from database again.
 | 
				
			||||||
 | 
					// Note: x.Iterate does not get latest data after insert/delete, so we have to call this function
 | 
				
			||||||
 | 
					// outside any session scope independently.
 | 
				
			||||||
 | 
					func RewriteAllPublicKeys() error {
 | 
				
			||||||
 | 
						return rewriteAllPublicKeys(x)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func rewriteAllPublicKeys(e Engine) error {
 | 
				
			||||||
 | 
						// Don't rewrite key if internal server
 | 
				
			||||||
 | 
						if setting.SSH.StartBuiltinServer || !setting.SSH.CreateAuthorizedKeysFile {
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						sshOpLocker.Lock()
 | 
				
			||||||
 | 
						defer sshOpLocker.Unlock()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if setting.SSH.RootPath != "" {
 | 
				
			||||||
 | 
							// First of ensure that the RootPath is present, and if not make it with 0700 permissions
 | 
				
			||||||
 | 
							// This of course doesn't guarantee that this is the right directory for authorized_keys
 | 
				
			||||||
 | 
							// but at least if it's supposed to be this directory and it doesn't exist and we're the
 | 
				
			||||||
 | 
							// right user it will at least be created properly.
 | 
				
			||||||
 | 
							err := os.MkdirAll(setting.SSH.RootPath, 0o700)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								log.Error("Unable to MkdirAll(%s): %v", setting.SSH.RootPath, err)
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						fPath := filepath.Join(setting.SSH.RootPath, "authorized_keys")
 | 
				
			||||||
 | 
						tmpPath := fPath + ".tmp"
 | 
				
			||||||
 | 
						t, err := os.OpenFile(tmpPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o600)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						defer func() {
 | 
				
			||||||
 | 
							t.Close()
 | 
				
			||||||
 | 
							if err := util.Remove(tmpPath); err != nil {
 | 
				
			||||||
 | 
								log.Warn("Unable to remove temporary authorized keys file: %s: Error: %v", tmpPath, err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if setting.SSH.AuthorizedKeysBackup {
 | 
				
			||||||
 | 
							isExist, err := util.IsExist(fPath)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								log.Error("Unable to check if %s exists. Error: %v", fPath, err)
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if isExist {
 | 
				
			||||||
 | 
								bakPath := fmt.Sprintf("%s_%d.gitea_bak", fPath, time.Now().Unix())
 | 
				
			||||||
 | 
								if err = util.CopyFile(fPath, bakPath); err != nil {
 | 
				
			||||||
 | 
									return err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := regeneratePublicKeys(e, t); err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						t.Close()
 | 
				
			||||||
 | 
						return util.Rename(tmpPath, fPath)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// RegeneratePublicKeys regenerates the authorized_keys file
 | 
				
			||||||
 | 
					func RegeneratePublicKeys(t io.StringWriter) error {
 | 
				
			||||||
 | 
						return regeneratePublicKeys(x, t)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func regeneratePublicKeys(e Engine, t io.StringWriter) error {
 | 
				
			||||||
 | 
						if err := e.Where("type != ?", KeyTypePrincipal).Iterate(new(PublicKey), func(idx int, bean interface{}) (err error) {
 | 
				
			||||||
 | 
							_, err = t.WriteString((bean.(*PublicKey)).AuthorizedString())
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}); err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						fPath := filepath.Join(setting.SSH.RootPath, "authorized_keys")
 | 
				
			||||||
 | 
						isExist, err := util.IsExist(fPath)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							log.Error("Unable to check if %s exists. Error: %v", fPath, err)
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if isExist {
 | 
				
			||||||
 | 
							f, err := os.Open(fPath)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							scanner := bufio.NewScanner(f)
 | 
				
			||||||
 | 
							for scanner.Scan() {
 | 
				
			||||||
 | 
								line := scanner.Text()
 | 
				
			||||||
 | 
								if strings.HasPrefix(line, tplCommentPrefix) {
 | 
				
			||||||
 | 
									scanner.Scan()
 | 
				
			||||||
 | 
									continue
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								_, err = t.WriteString(line + "\n")
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									f.Close()
 | 
				
			||||||
 | 
									return err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							f.Close()
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										142
									
								
								models/ssh_key_authorized_principals.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										142
									
								
								models/ssh_key_authorized_principals.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,142 @@
 | 
				
			|||||||
 | 
					// Copyright 2021 The Gitea Authors. All rights reserved.
 | 
				
			||||||
 | 
					// Use of this source code is governed by a MIT-style
 | 
				
			||||||
 | 
					// license that can be found in the LICENSE file.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"bufio"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"io"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"path/filepath"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/log"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/setting"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/util"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//  _____          __  .__                 .__                  .___
 | 
				
			||||||
 | 
					// /  _  \  __ ___/  |_|  |__   ___________|__|_______ ____   __| _/
 | 
				
			||||||
 | 
					// /  /_\  \|  |  \   __\  |  \ /  _ \_  __ \  \___   // __ \ / __ |
 | 
				
			||||||
 | 
					// /    |    \  |  /|  | |   Y  (  <_> )  | \/  |/    /\  ___// /_/ |
 | 
				
			||||||
 | 
					// \____|__  /____/ |__| |___|  /\____/|__|  |__/_____ \\___  >____ |
 | 
				
			||||||
 | 
					//         \/                 \/                      \/    \/     \/
 | 
				
			||||||
 | 
					// __________       .__              .__             .__
 | 
				
			||||||
 | 
					// \______   _______|__| ____   ____ |_____________  |  |   ______
 | 
				
			||||||
 | 
					//  |     ___\_  __ |  |/    \_/ ___\|  \____ \__  \ |  |  /  ___/
 | 
				
			||||||
 | 
					//  |    |    |  | \|  |   |  \  \___|  |  |_> / __ \|  |__\___ \
 | 
				
			||||||
 | 
					//  |____|    |__|  |__|___|  /\___  |__|   __(____  |____/____  >
 | 
				
			||||||
 | 
					//                          \/     \/   |__|       \/          \/
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// This file contains functions for creating authorized_principals files
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// There is a dependence on the database within RewriteAllPrincipalKeys & RegeneratePrincipalKeys
 | 
				
			||||||
 | 
					// The sshOpLocker is used from ssh_key_authorized_keys.go
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const authorizedPrincipalsFile = "authorized_principals"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// RewriteAllPrincipalKeys removes any authorized principal and rewrite all keys from database again.
 | 
				
			||||||
 | 
					// Note: x.Iterate does not get latest data after insert/delete, so we have to call this function
 | 
				
			||||||
 | 
					// outside any session scope independently.
 | 
				
			||||||
 | 
					func RewriteAllPrincipalKeys() error {
 | 
				
			||||||
 | 
						return rewriteAllPrincipalKeys(x)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func rewriteAllPrincipalKeys(e Engine) error {
 | 
				
			||||||
 | 
						// Don't rewrite key if internal server
 | 
				
			||||||
 | 
						if setting.SSH.StartBuiltinServer || !setting.SSH.CreateAuthorizedPrincipalsFile {
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						sshOpLocker.Lock()
 | 
				
			||||||
 | 
						defer sshOpLocker.Unlock()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if setting.SSH.RootPath != "" {
 | 
				
			||||||
 | 
							// First of ensure that the RootPath is present, and if not make it with 0700 permissions
 | 
				
			||||||
 | 
							// This of course doesn't guarantee that this is the right directory for authorized_keys
 | 
				
			||||||
 | 
							// but at least if it's supposed to be this directory and it doesn't exist and we're the
 | 
				
			||||||
 | 
							// right user it will at least be created properly.
 | 
				
			||||||
 | 
							err := os.MkdirAll(setting.SSH.RootPath, 0o700)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								log.Error("Unable to MkdirAll(%s): %v", setting.SSH.RootPath, err)
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						fPath := filepath.Join(setting.SSH.RootPath, authorizedPrincipalsFile)
 | 
				
			||||||
 | 
						tmpPath := fPath + ".tmp"
 | 
				
			||||||
 | 
						t, err := os.OpenFile(tmpPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o600)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						defer func() {
 | 
				
			||||||
 | 
							t.Close()
 | 
				
			||||||
 | 
							os.Remove(tmpPath)
 | 
				
			||||||
 | 
						}()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if setting.SSH.AuthorizedPrincipalsBackup {
 | 
				
			||||||
 | 
							isExist, err := util.IsExist(fPath)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								log.Error("Unable to check if %s exists. Error: %v", fPath, err)
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if isExist {
 | 
				
			||||||
 | 
								bakPath := fmt.Sprintf("%s_%d.gitea_bak", fPath, time.Now().Unix())
 | 
				
			||||||
 | 
								if err = util.CopyFile(fPath, bakPath); err != nil {
 | 
				
			||||||
 | 
									return err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := regeneratePrincipalKeys(e, t); err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						t.Close()
 | 
				
			||||||
 | 
						return util.Rename(tmpPath, fPath)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// RegeneratePrincipalKeys regenerates the authorized_principals file
 | 
				
			||||||
 | 
					func RegeneratePrincipalKeys(t io.StringWriter) error {
 | 
				
			||||||
 | 
						return regeneratePrincipalKeys(x, t)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func regeneratePrincipalKeys(e Engine, t io.StringWriter) error {
 | 
				
			||||||
 | 
						if err := e.Where("type = ?", KeyTypePrincipal).Iterate(new(PublicKey), func(idx int, bean interface{}) (err error) {
 | 
				
			||||||
 | 
							_, err = t.WriteString((bean.(*PublicKey)).AuthorizedString())
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}); err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						fPath := filepath.Join(setting.SSH.RootPath, authorizedPrincipalsFile)
 | 
				
			||||||
 | 
						isExist, err := util.IsExist(fPath)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							log.Error("Unable to check if %s exists. Error: %v", fPath, err)
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if isExist {
 | 
				
			||||||
 | 
							f, err := os.Open(fPath)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							scanner := bufio.NewScanner(f)
 | 
				
			||||||
 | 
							for scanner.Scan() {
 | 
				
			||||||
 | 
								line := scanner.Text()
 | 
				
			||||||
 | 
								if strings.HasPrefix(line, tplCommentPrefix) {
 | 
				
			||||||
 | 
									scanner.Scan()
 | 
				
			||||||
 | 
									continue
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								_, err = t.WriteString(line + "\n")
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									f.Close()
 | 
				
			||||||
 | 
									return err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							f.Close()
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										299
									
								
								models/ssh_key_deploy.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										299
									
								
								models/ssh_key_deploy.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,299 @@
 | 
				
			|||||||
 | 
					// Copyright 2021 The Gitea Authors. All rights reserved.
 | 
				
			||||||
 | 
					// Use of this source code is governed by a MIT-style
 | 
				
			||||||
 | 
					// license that can be found in the LICENSE file.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/timeutil"
 | 
				
			||||||
 | 
						"xorm.io/builder"
 | 
				
			||||||
 | 
						"xorm.io/xorm"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ________                .__                 ____  __.
 | 
				
			||||||
 | 
					// \______ \   ____ ______ |  |   ____ ___.__.|    |/ _|____ ___.__.
 | 
				
			||||||
 | 
					//  |    |  \_/ __ \\____ \|  |  /  _ <   |  ||      <_/ __ <   |  |
 | 
				
			||||||
 | 
					//  |    `   \  ___/|  |_> >  |_(  <_> )___  ||    |  \  ___/\___  |
 | 
				
			||||||
 | 
					// /_______  /\___  >   __/|____/\____// ____||____|__ \___  > ____|
 | 
				
			||||||
 | 
					//         \/     \/|__|               \/             \/   \/\/
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// This file contains functions specific to DeployKeys
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// DeployKey represents deploy key information and its relation with repository.
 | 
				
			||||||
 | 
					type DeployKey struct {
 | 
				
			||||||
 | 
						ID          int64 `xorm:"pk autoincr"`
 | 
				
			||||||
 | 
						KeyID       int64 `xorm:"UNIQUE(s) INDEX"`
 | 
				
			||||||
 | 
						RepoID      int64 `xorm:"UNIQUE(s) INDEX"`
 | 
				
			||||||
 | 
						Name        string
 | 
				
			||||||
 | 
						Fingerprint string
 | 
				
			||||||
 | 
						Content     string `xorm:"-"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						Mode AccessMode `xorm:"NOT NULL DEFAULT 1"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						CreatedUnix       timeutil.TimeStamp `xorm:"created"`
 | 
				
			||||||
 | 
						UpdatedUnix       timeutil.TimeStamp `xorm:"updated"`
 | 
				
			||||||
 | 
						HasRecentActivity bool               `xorm:"-"`
 | 
				
			||||||
 | 
						HasUsed           bool               `xorm:"-"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// AfterLoad is invoked from XORM after setting the values of all fields of this object.
 | 
				
			||||||
 | 
					func (key *DeployKey) AfterLoad() {
 | 
				
			||||||
 | 
						key.HasUsed = key.UpdatedUnix > key.CreatedUnix
 | 
				
			||||||
 | 
						key.HasRecentActivity = key.UpdatedUnix.AddDuration(7*24*time.Hour) > timeutil.TimeStampNow()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetContent gets associated public key content.
 | 
				
			||||||
 | 
					func (key *DeployKey) GetContent() error {
 | 
				
			||||||
 | 
						pkey, err := GetPublicKeyByID(key.KeyID)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						key.Content = pkey.Content
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// IsReadOnly checks if the key can only be used for read operations
 | 
				
			||||||
 | 
					func (key *DeployKey) IsReadOnly() bool {
 | 
				
			||||||
 | 
						return key.Mode == AccessModeRead
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func checkDeployKey(e Engine, keyID, repoID int64, name string) error {
 | 
				
			||||||
 | 
						// Note: We want error detail, not just true or false here.
 | 
				
			||||||
 | 
						has, err := e.
 | 
				
			||||||
 | 
							Where("key_id = ? AND repo_id = ?", keyID, repoID).
 | 
				
			||||||
 | 
							Get(new(DeployKey))
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						} else if has {
 | 
				
			||||||
 | 
							return ErrDeployKeyAlreadyExist{keyID, repoID}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						has, err = e.
 | 
				
			||||||
 | 
							Where("repo_id = ? AND name = ?", repoID, name).
 | 
				
			||||||
 | 
							Get(new(DeployKey))
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						} else if has {
 | 
				
			||||||
 | 
							return ErrDeployKeyNameAlreadyUsed{repoID, name}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// addDeployKey adds new key-repo relation.
 | 
				
			||||||
 | 
					func addDeployKey(e *xorm.Session, keyID, repoID int64, name, fingerprint string, mode AccessMode) (*DeployKey, error) {
 | 
				
			||||||
 | 
						if err := checkDeployKey(e, keyID, repoID, name); err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						key := &DeployKey{
 | 
				
			||||||
 | 
							KeyID:       keyID,
 | 
				
			||||||
 | 
							RepoID:      repoID,
 | 
				
			||||||
 | 
							Name:        name,
 | 
				
			||||||
 | 
							Fingerprint: fingerprint,
 | 
				
			||||||
 | 
							Mode:        mode,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						_, err := e.Insert(key)
 | 
				
			||||||
 | 
						return key, err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// HasDeployKey returns true if public key is a deploy key of given repository.
 | 
				
			||||||
 | 
					func HasDeployKey(keyID, repoID int64) bool {
 | 
				
			||||||
 | 
						has, _ := x.
 | 
				
			||||||
 | 
							Where("key_id = ? AND repo_id = ?", keyID, repoID).
 | 
				
			||||||
 | 
							Get(new(DeployKey))
 | 
				
			||||||
 | 
						return has
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// AddDeployKey add new deploy key to database and authorized_keys file.
 | 
				
			||||||
 | 
					func AddDeployKey(repoID int64, name, content string, readOnly bool) (*DeployKey, error) {
 | 
				
			||||||
 | 
						fingerprint, err := calcFingerprint(content)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						accessMode := AccessModeRead
 | 
				
			||||||
 | 
						if !readOnly {
 | 
				
			||||||
 | 
							accessMode = AccessModeWrite
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						sess := x.NewSession()
 | 
				
			||||||
 | 
						defer sess.Close()
 | 
				
			||||||
 | 
						if err = sess.Begin(); err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						pkey := &PublicKey{
 | 
				
			||||||
 | 
							Fingerprint: fingerprint,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						has, err := sess.Get(pkey)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if has {
 | 
				
			||||||
 | 
							if pkey.Type != KeyTypeDeploy {
 | 
				
			||||||
 | 
								return nil, ErrKeyAlreadyExist{0, fingerprint, ""}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							// First time use this deploy key.
 | 
				
			||||||
 | 
							pkey.Mode = accessMode
 | 
				
			||||||
 | 
							pkey.Type = KeyTypeDeploy
 | 
				
			||||||
 | 
							pkey.Content = content
 | 
				
			||||||
 | 
							pkey.Name = name
 | 
				
			||||||
 | 
							if err = addKey(sess, pkey); err != nil {
 | 
				
			||||||
 | 
								return nil, fmt.Errorf("addKey: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						key, err := addDeployKey(sess, pkey.ID, repoID, name, pkey.Fingerprint, accessMode)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return key, sess.Commit()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetDeployKeyByID returns deploy key by given ID.
 | 
				
			||||||
 | 
					func GetDeployKeyByID(id int64) (*DeployKey, error) {
 | 
				
			||||||
 | 
						return getDeployKeyByID(x, id)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func getDeployKeyByID(e Engine, id int64) (*DeployKey, error) {
 | 
				
			||||||
 | 
						key := new(DeployKey)
 | 
				
			||||||
 | 
						has, err := e.ID(id).Get(key)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						} else if !has {
 | 
				
			||||||
 | 
							return nil, ErrDeployKeyNotExist{id, 0, 0}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return key, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetDeployKeyByRepo returns deploy key by given public key ID and repository ID.
 | 
				
			||||||
 | 
					func GetDeployKeyByRepo(keyID, repoID int64) (*DeployKey, error) {
 | 
				
			||||||
 | 
						return getDeployKeyByRepo(x, keyID, repoID)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func getDeployKeyByRepo(e Engine, keyID, repoID int64) (*DeployKey, error) {
 | 
				
			||||||
 | 
						key := &DeployKey{
 | 
				
			||||||
 | 
							KeyID:  keyID,
 | 
				
			||||||
 | 
							RepoID: repoID,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						has, err := e.Get(key)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						} else if !has {
 | 
				
			||||||
 | 
							return nil, ErrDeployKeyNotExist{0, keyID, repoID}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return key, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// UpdateDeployKeyCols updates deploy key information in the specified columns.
 | 
				
			||||||
 | 
					func UpdateDeployKeyCols(key *DeployKey, cols ...string) error {
 | 
				
			||||||
 | 
						_, err := x.ID(key.ID).Cols(cols...).Update(key)
 | 
				
			||||||
 | 
						return err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// UpdateDeployKey updates deploy key information.
 | 
				
			||||||
 | 
					func UpdateDeployKey(key *DeployKey) error {
 | 
				
			||||||
 | 
						_, err := x.ID(key.ID).AllCols().Update(key)
 | 
				
			||||||
 | 
						return err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// DeleteDeployKey deletes deploy key from its repository authorized_keys file if needed.
 | 
				
			||||||
 | 
					func DeleteDeployKey(doer *User, id int64) error {
 | 
				
			||||||
 | 
						sess := x.NewSession()
 | 
				
			||||||
 | 
						defer sess.Close()
 | 
				
			||||||
 | 
						if err := sess.Begin(); err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if err := deleteDeployKey(sess, doer, id); err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return sess.Commit()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func deleteDeployKey(sess Engine, doer *User, id int64) error {
 | 
				
			||||||
 | 
						key, err := getDeployKeyByID(sess, id)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							if IsErrDeployKeyNotExist(err) {
 | 
				
			||||||
 | 
								return nil
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return fmt.Errorf("GetDeployKeyByID: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Check if user has access to delete this key.
 | 
				
			||||||
 | 
						if !doer.IsAdmin {
 | 
				
			||||||
 | 
							repo, err := getRepositoryByID(sess, key.RepoID)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return fmt.Errorf("GetRepositoryByID: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							has, err := isUserRepoAdmin(sess, repo, doer)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return fmt.Errorf("GetUserRepoPermission: %v", err)
 | 
				
			||||||
 | 
							} else if !has {
 | 
				
			||||||
 | 
								return ErrKeyAccessDenied{doer.ID, key.ID, "deploy"}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if _, err = sess.ID(key.ID).Delete(new(DeployKey)); err != nil {
 | 
				
			||||||
 | 
							return fmt.Errorf("delete deploy key [%d]: %v", key.ID, err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Check if this is the last reference to same key content.
 | 
				
			||||||
 | 
						has, err := sess.
 | 
				
			||||||
 | 
							Where("key_id = ?", key.KeyID).
 | 
				
			||||||
 | 
							Get(new(DeployKey))
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						} else if !has {
 | 
				
			||||||
 | 
							if err = deletePublicKeys(sess, key.KeyID); err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// after deleted the public keys, should rewrite the public keys file
 | 
				
			||||||
 | 
							if err = rewriteAllPublicKeys(sess); err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ListDeployKeys returns all deploy keys by given repository ID.
 | 
				
			||||||
 | 
					func ListDeployKeys(repoID int64, listOptions ListOptions) ([]*DeployKey, error) {
 | 
				
			||||||
 | 
						return listDeployKeys(x, repoID, listOptions)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func listDeployKeys(e Engine, repoID int64, listOptions ListOptions) ([]*DeployKey, error) {
 | 
				
			||||||
 | 
						sess := e.Where("repo_id = ?", repoID)
 | 
				
			||||||
 | 
						if listOptions.Page != 0 {
 | 
				
			||||||
 | 
							sess = listOptions.setSessionPagination(sess)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							keys := make([]*DeployKey, 0, listOptions.PageSize)
 | 
				
			||||||
 | 
							return keys, sess.Find(&keys)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						keys := make([]*DeployKey, 0, 5)
 | 
				
			||||||
 | 
						return keys, sess.Find(&keys)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// SearchDeployKeys returns a list of deploy keys matching the provided arguments.
 | 
				
			||||||
 | 
					func SearchDeployKeys(repoID, keyID int64, fingerprint string) ([]*DeployKey, error) {
 | 
				
			||||||
 | 
						keys := make([]*DeployKey, 0, 5)
 | 
				
			||||||
 | 
						cond := builder.NewCond()
 | 
				
			||||||
 | 
						if repoID != 0 {
 | 
				
			||||||
 | 
							cond = cond.And(builder.Eq{"repo_id": repoID})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if keyID != 0 {
 | 
				
			||||||
 | 
							cond = cond.And(builder.Eq{"key_id": keyID})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if fingerprint != "" {
 | 
				
			||||||
 | 
							cond = cond.And(builder.Eq{"fingerprint": fingerprint})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return keys, x.Where(cond).Find(&keys)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										97
									
								
								models/ssh_key_fingerprint.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								models/ssh_key_fingerprint.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,97 @@
 | 
				
			|||||||
 | 
					// Copyright 2021 The Gitea Authors. All rights reserved.
 | 
				
			||||||
 | 
					// Use of this source code is governed by a MIT-style
 | 
				
			||||||
 | 
					// license that can be found in the LICENSE file.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/log"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/process"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/setting"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/util"
 | 
				
			||||||
 | 
						"golang.org/x/crypto/ssh"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ___________.__                                         .__        __
 | 
				
			||||||
 | 
					// \_   _____/|__| ____    ____   ________________________|__| _____/  |_
 | 
				
			||||||
 | 
					//  |    __)  |  |/    \  / ___\_/ __ \_  __ \____ \_  __ \  |/    \   __\
 | 
				
			||||||
 | 
					//  |     \   |  |   |  \/ /_/  >  ___/|  | \/  |_> >  | \/  |   |  \  |
 | 
				
			||||||
 | 
					//  \___  /   |__|___|  /\___  / \___  >__|  |   __/|__|  |__|___|  /__|
 | 
				
			||||||
 | 
					//      \/            \//_____/      \/      |__|                 \/
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// This file contains functions for fingerprinting SSH keys
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// The database is used in checkKeyFingerprint however most of these functions probably belong in a module
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// checkKeyFingerprint only checks if key fingerprint has been used as public key,
 | 
				
			||||||
 | 
					// it is OK to use same key as deploy key for multiple repositories/users.
 | 
				
			||||||
 | 
					func checkKeyFingerprint(e Engine, fingerprint string) error {
 | 
				
			||||||
 | 
						has, err := e.Get(&PublicKey{
 | 
				
			||||||
 | 
							Fingerprint: fingerprint,
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						} else if has {
 | 
				
			||||||
 | 
							return ErrKeyAlreadyExist{0, fingerprint, ""}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func calcFingerprintSSHKeygen(publicKeyContent string) (string, error) {
 | 
				
			||||||
 | 
						// Calculate fingerprint.
 | 
				
			||||||
 | 
						tmpPath, err := writeTmpKeyFile(publicKeyContent)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return "", err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						defer func() {
 | 
				
			||||||
 | 
							if err := util.Remove(tmpPath); err != nil {
 | 
				
			||||||
 | 
								log.Warn("Unable to remove temporary key file: %s: Error: %v", tmpPath, err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}()
 | 
				
			||||||
 | 
						stdout, stderr, err := process.GetManager().Exec("AddPublicKey", "ssh-keygen", "-lf", tmpPath)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							if strings.Contains(stderr, "is not a public key file") {
 | 
				
			||||||
 | 
								return "", ErrKeyUnableVerify{stderr}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return "", fmt.Errorf("'ssh-keygen -lf %s' failed with error '%s': %s", tmpPath, err, stderr)
 | 
				
			||||||
 | 
						} else if len(stdout) < 2 {
 | 
				
			||||||
 | 
							return "", errors.New("not enough output for calculating fingerprint: " + stdout)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return strings.Split(stdout, " ")[1], nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func calcFingerprintNative(publicKeyContent string) (string, error) {
 | 
				
			||||||
 | 
						// Calculate fingerprint.
 | 
				
			||||||
 | 
						pk, _, _, _, err := ssh.ParseAuthorizedKey([]byte(publicKeyContent))
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return "", err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return ssh.FingerprintSHA256(pk), nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func calcFingerprint(publicKeyContent string) (string, error) {
 | 
				
			||||||
 | 
						// Call the method based on configuration
 | 
				
			||||||
 | 
						var (
 | 
				
			||||||
 | 
							fnName, fp string
 | 
				
			||||||
 | 
							err        error
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						if setting.SSH.StartBuiltinServer {
 | 
				
			||||||
 | 
							fnName = "calcFingerprintNative"
 | 
				
			||||||
 | 
							fp, err = calcFingerprintNative(publicKeyContent)
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							fnName = "calcFingerprintSSHKeygen"
 | 
				
			||||||
 | 
							fp, err = calcFingerprintSSHKeygen(publicKeyContent)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							if IsErrKeyUnableVerify(err) {
 | 
				
			||||||
 | 
								log.Info("%s", publicKeyContent)
 | 
				
			||||||
 | 
								return "", err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return "", fmt.Errorf("%s: %v", fnName, err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return fp, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										309
									
								
								models/ssh_key_parse.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										309
									
								
								models/ssh_key_parse.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,309 @@
 | 
				
			|||||||
 | 
					// Copyright 2021 The Gitea Authors. All rights reserved.
 | 
				
			||||||
 | 
					// Use of this source code is governed by a MIT-style
 | 
				
			||||||
 | 
					// license that can be found in the LICENSE file.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"crypto/rsa"
 | 
				
			||||||
 | 
						"crypto/x509"
 | 
				
			||||||
 | 
						"encoding/asn1"
 | 
				
			||||||
 | 
						"encoding/base64"
 | 
				
			||||||
 | 
						"encoding/binary"
 | 
				
			||||||
 | 
						"encoding/pem"
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"io/ioutil"
 | 
				
			||||||
 | 
						"math/big"
 | 
				
			||||||
 | 
						"strconv"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/log"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/process"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/setting"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/util"
 | 
				
			||||||
 | 
						"golang.org/x/crypto/ssh"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//  ____  __.             __________
 | 
				
			||||||
 | 
					// |    |/ _|____ ___.__. \______   \_____ _______  ______ ___________
 | 
				
			||||||
 | 
					// |      <_/ __ <   |  |  |     ___/\__  \\_  __ \/  ___// __ \_  __ \
 | 
				
			||||||
 | 
					// |    |  \  ___/\___  |  |    |     / __ \|  | \/\___ \\  ___/|  | \/
 | 
				
			||||||
 | 
					// |____|__ \___  > ____|  |____|    (____  /__|  /____  >\___  >__|
 | 
				
			||||||
 | 
					//         \/   \/\/                      \/           \/     \/
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// This file contains functiosn for parsing ssh-keys
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// TODO: Consider if these functions belong in models - no other models function call them or are called by them
 | 
				
			||||||
 | 
					// They may belong in a service or a module
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const ssh2keyStart = "---- BEGIN SSH2 PUBLIC KEY ----"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func extractTypeFromBase64Key(key string) (string, error) {
 | 
				
			||||||
 | 
						b, err := base64.StdEncoding.DecodeString(key)
 | 
				
			||||||
 | 
						if err != nil || len(b) < 4 {
 | 
				
			||||||
 | 
							return "", fmt.Errorf("invalid key format: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						keyLength := int(binary.BigEndian.Uint32(b))
 | 
				
			||||||
 | 
						if len(b) < 4+keyLength {
 | 
				
			||||||
 | 
							return "", fmt.Errorf("invalid key format: not enough length %d", keyLength)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return string(b[4 : 4+keyLength]), nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// parseKeyString parses any key string in OpenSSH or SSH2 format to clean OpenSSH string (RFC4253).
 | 
				
			||||||
 | 
					func parseKeyString(content string) (string, error) {
 | 
				
			||||||
 | 
						// remove whitespace at start and end
 | 
				
			||||||
 | 
						content = strings.TrimSpace(content)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var keyType, keyContent, keyComment string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if strings.HasPrefix(content, ssh2keyStart) {
 | 
				
			||||||
 | 
							// Parse SSH2 file format.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Transform all legal line endings to a single "\n".
 | 
				
			||||||
 | 
							content = strings.NewReplacer("\r\n", "\n", "\r", "\n").Replace(content)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							lines := strings.Split(content, "\n")
 | 
				
			||||||
 | 
							continuationLine := false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							for _, line := range lines {
 | 
				
			||||||
 | 
								// Skip lines that:
 | 
				
			||||||
 | 
								// 1) are a continuation of the previous line,
 | 
				
			||||||
 | 
								// 2) contain ":" as that are comment lines
 | 
				
			||||||
 | 
								// 3) contain "-" as that are begin and end tags
 | 
				
			||||||
 | 
								if continuationLine || strings.ContainsAny(line, ":-") {
 | 
				
			||||||
 | 
									continuationLine = strings.HasSuffix(line, "\\")
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									keyContent += line
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							t, err := extractTypeFromBase64Key(keyContent)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return "", fmt.Errorf("extractTypeFromBase64Key: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							keyType = t
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							if strings.Contains(content, "-----BEGIN") {
 | 
				
			||||||
 | 
								// Convert PEM Keys to OpenSSH format
 | 
				
			||||||
 | 
								// Transform all legal line endings to a single "\n".
 | 
				
			||||||
 | 
								content = strings.NewReplacer("\r\n", "\n", "\r", "\n").Replace(content)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								block, _ := pem.Decode([]byte(content))
 | 
				
			||||||
 | 
								if block == nil {
 | 
				
			||||||
 | 
									return "", fmt.Errorf("failed to parse PEM block containing the public key")
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								pub, err := x509.ParsePKIXPublicKey(block.Bytes)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									var pk rsa.PublicKey
 | 
				
			||||||
 | 
									_, err2 := asn1.Unmarshal(block.Bytes, &pk)
 | 
				
			||||||
 | 
									if err2 != nil {
 | 
				
			||||||
 | 
										return "", fmt.Errorf("failed to parse DER encoded public key as either PKIX or PEM RSA Key: %v %v", err, err2)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									pub = &pk
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								sshKey, err := ssh.NewPublicKey(pub)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									return "", fmt.Errorf("unable to convert to ssh public key: %v", err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								content = string(ssh.MarshalAuthorizedKey(sshKey))
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							// Parse OpenSSH format.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Remove all newlines
 | 
				
			||||||
 | 
							content = strings.NewReplacer("\r\n", "", "\n", "").Replace(content)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							parts := strings.SplitN(content, " ", 3)
 | 
				
			||||||
 | 
							switch len(parts) {
 | 
				
			||||||
 | 
							case 0:
 | 
				
			||||||
 | 
								return "", errors.New("empty key")
 | 
				
			||||||
 | 
							case 1:
 | 
				
			||||||
 | 
								keyContent = parts[0]
 | 
				
			||||||
 | 
							case 2:
 | 
				
			||||||
 | 
								keyType = parts[0]
 | 
				
			||||||
 | 
								keyContent = parts[1]
 | 
				
			||||||
 | 
							default:
 | 
				
			||||||
 | 
								keyType = parts[0]
 | 
				
			||||||
 | 
								keyContent = parts[1]
 | 
				
			||||||
 | 
								keyComment = parts[2]
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// If keyType is not given, extract it from content. If given, validate it.
 | 
				
			||||||
 | 
							t, err := extractTypeFromBase64Key(keyContent)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return "", fmt.Errorf("extractTypeFromBase64Key: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if len(keyType) == 0 {
 | 
				
			||||||
 | 
								keyType = t
 | 
				
			||||||
 | 
							} else if keyType != t {
 | 
				
			||||||
 | 
								return "", fmt.Errorf("key type and content does not match: %s - %s", keyType, t)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// Finally we need to check whether we can actually read the proposed key:
 | 
				
			||||||
 | 
						_, _, _, _, err := ssh.ParseAuthorizedKey([]byte(keyType + " " + keyContent + " " + keyComment))
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return "", fmt.Errorf("invalid ssh public key: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return keyType + " " + keyContent + " " + keyComment, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// CheckPublicKeyString checks if the given public key string is recognized by SSH.
 | 
				
			||||||
 | 
					// It returns the actual public key line on success.
 | 
				
			||||||
 | 
					func CheckPublicKeyString(content string) (_ string, err error) {
 | 
				
			||||||
 | 
						if setting.SSH.Disabled {
 | 
				
			||||||
 | 
							return "", ErrSSHDisabled{}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						content, err = parseKeyString(content)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return "", err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						content = strings.TrimRight(content, "\n\r")
 | 
				
			||||||
 | 
						if strings.ContainsAny(content, "\n\r") {
 | 
				
			||||||
 | 
							return "", errors.New("only a single line with a single key please")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// remove any unnecessary whitespace now
 | 
				
			||||||
 | 
						content = strings.TrimSpace(content)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if !setting.SSH.MinimumKeySizeCheck {
 | 
				
			||||||
 | 
							return content, nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var (
 | 
				
			||||||
 | 
							fnName  string
 | 
				
			||||||
 | 
							keyType string
 | 
				
			||||||
 | 
							length  int
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						if setting.SSH.StartBuiltinServer {
 | 
				
			||||||
 | 
							fnName = "SSHNativeParsePublicKey"
 | 
				
			||||||
 | 
							keyType, length, err = SSHNativeParsePublicKey(content)
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							fnName = "SSHKeyGenParsePublicKey"
 | 
				
			||||||
 | 
							keyType, length, err = SSHKeyGenParsePublicKey(content)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return "", fmt.Errorf("%s: %v", fnName, err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						log.Trace("Key info [native: %v]: %s-%d", setting.SSH.StartBuiltinServer, keyType, length)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if minLen, found := setting.SSH.MinimumKeySizes[keyType]; found && length >= minLen {
 | 
				
			||||||
 | 
							return content, nil
 | 
				
			||||||
 | 
						} else if found && length < minLen {
 | 
				
			||||||
 | 
							return "", fmt.Errorf("key length is not enough: got %d, needs %d", length, minLen)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return "", fmt.Errorf("key type is not allowed: %s", keyType)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// SSHNativeParsePublicKey extracts the key type and length using the golang SSH library.
 | 
				
			||||||
 | 
					func SSHNativeParsePublicKey(keyLine string) (string, int, error) {
 | 
				
			||||||
 | 
						fields := strings.Fields(keyLine)
 | 
				
			||||||
 | 
						if len(fields) < 2 {
 | 
				
			||||||
 | 
							return "", 0, fmt.Errorf("not enough fields in public key line: %s", keyLine)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						raw, err := base64.StdEncoding.DecodeString(fields[1])
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return "", 0, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						pkey, err := ssh.ParsePublicKey(raw)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							if strings.Contains(err.Error(), "ssh: unknown key algorithm") {
 | 
				
			||||||
 | 
								return "", 0, ErrKeyUnableVerify{err.Error()}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return "", 0, fmt.Errorf("ParsePublicKey: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// The ssh library can parse the key, so next we find out what key exactly we have.
 | 
				
			||||||
 | 
						switch pkey.Type() {
 | 
				
			||||||
 | 
						case ssh.KeyAlgoDSA:
 | 
				
			||||||
 | 
							rawPub := struct {
 | 
				
			||||||
 | 
								Name       string
 | 
				
			||||||
 | 
								P, Q, G, Y *big.Int
 | 
				
			||||||
 | 
							}{}
 | 
				
			||||||
 | 
							if err := ssh.Unmarshal(pkey.Marshal(), &rawPub); err != nil {
 | 
				
			||||||
 | 
								return "", 0, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							// as per https://bugzilla.mindrot.org/show_bug.cgi?id=1647 we should never
 | 
				
			||||||
 | 
							// see dsa keys != 1024 bit, but as it seems to work, we will not check here
 | 
				
			||||||
 | 
							return "dsa", rawPub.P.BitLen(), nil // use P as per crypto/dsa/dsa.go (is L)
 | 
				
			||||||
 | 
						case ssh.KeyAlgoRSA:
 | 
				
			||||||
 | 
							rawPub := struct {
 | 
				
			||||||
 | 
								Name string
 | 
				
			||||||
 | 
								E    *big.Int
 | 
				
			||||||
 | 
								N    *big.Int
 | 
				
			||||||
 | 
							}{}
 | 
				
			||||||
 | 
							if err := ssh.Unmarshal(pkey.Marshal(), &rawPub); err != nil {
 | 
				
			||||||
 | 
								return "", 0, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return "rsa", rawPub.N.BitLen(), nil // use N as per crypto/rsa/rsa.go (is bits)
 | 
				
			||||||
 | 
						case ssh.KeyAlgoECDSA256:
 | 
				
			||||||
 | 
							return "ecdsa", 256, nil
 | 
				
			||||||
 | 
						case ssh.KeyAlgoECDSA384:
 | 
				
			||||||
 | 
							return "ecdsa", 384, nil
 | 
				
			||||||
 | 
						case ssh.KeyAlgoECDSA521:
 | 
				
			||||||
 | 
							return "ecdsa", 521, nil
 | 
				
			||||||
 | 
						case ssh.KeyAlgoED25519:
 | 
				
			||||||
 | 
							return "ed25519", 256, nil
 | 
				
			||||||
 | 
						case ssh.KeyAlgoSKECDSA256:
 | 
				
			||||||
 | 
							return "ecdsa-sk", 256, nil
 | 
				
			||||||
 | 
						case ssh.KeyAlgoSKED25519:
 | 
				
			||||||
 | 
							return "ed25519-sk", 256, nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return "", 0, fmt.Errorf("unsupported key length detection for type: %s", pkey.Type())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// writeTmpKeyFile writes key content to a temporary file
 | 
				
			||||||
 | 
					// and returns the name of that file, along with any possible errors.
 | 
				
			||||||
 | 
					func writeTmpKeyFile(content string) (string, error) {
 | 
				
			||||||
 | 
						tmpFile, err := ioutil.TempFile(setting.SSH.KeyTestPath, "gitea_keytest")
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return "", fmt.Errorf("TempFile: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						defer tmpFile.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if _, err = tmpFile.WriteString(content); err != nil {
 | 
				
			||||||
 | 
							return "", fmt.Errorf("WriteString: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return tmpFile.Name(), nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// SSHKeyGenParsePublicKey extracts key type and length using ssh-keygen.
 | 
				
			||||||
 | 
					func SSHKeyGenParsePublicKey(key string) (string, int, error) {
 | 
				
			||||||
 | 
						tmpName, err := writeTmpKeyFile(key)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return "", 0, fmt.Errorf("writeTmpKeyFile: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						defer func() {
 | 
				
			||||||
 | 
							if err := util.Remove(tmpName); err != nil {
 | 
				
			||||||
 | 
								log.Warn("Unable to remove temporary key file: %s: Error: %v", tmpName, err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						stdout, stderr, err := process.GetManager().Exec("SSHKeyGenParsePublicKey", setting.SSH.KeygenPath, "-lf", tmpName)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return "", 0, fmt.Errorf("fail to parse public key: %s - %s", err, stderr)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if strings.Contains(stdout, "is not a public key file") {
 | 
				
			||||||
 | 
							return "", 0, ErrKeyUnableVerify{stdout}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						fields := strings.Split(stdout, " ")
 | 
				
			||||||
 | 
						if len(fields) < 4 {
 | 
				
			||||||
 | 
							return "", 0, fmt.Errorf("invalid public key line: %s", stdout)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						keyType := strings.Trim(fields[len(fields)-1], "()\r\n")
 | 
				
			||||||
 | 
						length, err := strconv.ParseInt(fields[0], 10, 32)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return "", 0, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return strings.ToLower(keyType), int(length), nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										125
									
								
								models/ssh_key_principals.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										125
									
								
								models/ssh_key_principals.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,125 @@
 | 
				
			|||||||
 | 
					// Copyright 2021 The Gitea Authors. All rights reserved.
 | 
				
			||||||
 | 
					// Use of this source code is governed by a MIT-style
 | 
				
			||||||
 | 
					// license that can be found in the LICENSE file.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/setting"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// __________       .__              .__             .__
 | 
				
			||||||
 | 
					// \______   _______|__| ____   ____ |_____________  |  |   ______
 | 
				
			||||||
 | 
					//  |     ___\_  __ |  |/    \_/ ___\|  \____ \__  \ |  |  /  ___/
 | 
				
			||||||
 | 
					//  |    |    |  | \|  |   |  \  \___|  |  |_> / __ \|  |__\___ \
 | 
				
			||||||
 | 
					//  |____|    |__|  |__|___|  /\___  |__|   __(____  |____/____  >
 | 
				
			||||||
 | 
					//                          \/     \/   |__|       \/          \/
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// This file contains functions related to principals
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// AddPrincipalKey adds new principal to database and authorized_principals file.
 | 
				
			||||||
 | 
					func AddPrincipalKey(ownerID int64, content string, loginSourceID int64) (*PublicKey, error) {
 | 
				
			||||||
 | 
						sess := x.NewSession()
 | 
				
			||||||
 | 
						defer sess.Close()
 | 
				
			||||||
 | 
						if err := sess.Begin(); err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Principals cannot be duplicated.
 | 
				
			||||||
 | 
						has, err := sess.
 | 
				
			||||||
 | 
							Where("content = ? AND type = ?", content, KeyTypePrincipal).
 | 
				
			||||||
 | 
							Get(new(PublicKey))
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						} else if has {
 | 
				
			||||||
 | 
							return nil, ErrKeyAlreadyExist{0, "", content}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						key := &PublicKey{
 | 
				
			||||||
 | 
							OwnerID:       ownerID,
 | 
				
			||||||
 | 
							Name:          content,
 | 
				
			||||||
 | 
							Content:       content,
 | 
				
			||||||
 | 
							Mode:          AccessModeWrite,
 | 
				
			||||||
 | 
							Type:          KeyTypePrincipal,
 | 
				
			||||||
 | 
							LoginSourceID: loginSourceID,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if err = addPrincipalKey(sess, key); err != nil {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("addKey: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err = sess.Commit(); err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						sess.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return key, RewriteAllPrincipalKeys()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func addPrincipalKey(e Engine, key *PublicKey) (err error) {
 | 
				
			||||||
 | 
						// Save Key representing a principal.
 | 
				
			||||||
 | 
						if _, err = e.Insert(key); err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// CheckPrincipalKeyString strips spaces and returns an error if the given principal contains newlines
 | 
				
			||||||
 | 
					func CheckPrincipalKeyString(user *User, content string) (_ string, err error) {
 | 
				
			||||||
 | 
						if setting.SSH.Disabled {
 | 
				
			||||||
 | 
							return "", ErrSSHDisabled{}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						content = strings.TrimSpace(content)
 | 
				
			||||||
 | 
						if strings.ContainsAny(content, "\r\n") {
 | 
				
			||||||
 | 
							return "", errors.New("only a single line with a single principal please")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// check all the allowed principals, email, username or anything
 | 
				
			||||||
 | 
						// if any matches, return ok
 | 
				
			||||||
 | 
						for _, v := range setting.SSH.AuthorizedPrincipalsAllow {
 | 
				
			||||||
 | 
							switch v {
 | 
				
			||||||
 | 
							case "anything":
 | 
				
			||||||
 | 
								return content, nil
 | 
				
			||||||
 | 
							case "email":
 | 
				
			||||||
 | 
								emails, err := GetEmailAddresses(user.ID)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									return "", err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								for _, email := range emails {
 | 
				
			||||||
 | 
									if !email.IsActivated {
 | 
				
			||||||
 | 
										continue
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									if content == email.Email {
 | 
				
			||||||
 | 
										return content, nil
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							case "username":
 | 
				
			||||||
 | 
								if content == user.Name {
 | 
				
			||||||
 | 
									return content, nil
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return "", fmt.Errorf("didn't match allowed principals: %s", setting.SSH.AuthorizedPrincipalsAllow)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ListPrincipalKeys returns a list of principals belongs to given user.
 | 
				
			||||||
 | 
					func ListPrincipalKeys(uid int64, listOptions ListOptions) ([]*PublicKey, error) {
 | 
				
			||||||
 | 
						sess := x.Where("owner_id = ? AND type = ?", uid, KeyTypePrincipal)
 | 
				
			||||||
 | 
						if listOptions.Page != 0 {
 | 
				
			||||||
 | 
							sess = listOptions.setSessionPagination(sess)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							keys := make([]*PublicKey, 0, listOptions.PageSize)
 | 
				
			||||||
 | 
							return keys, sess.Find(&keys)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						keys := make([]*PublicKey, 0, 5)
 | 
				
			||||||
 | 
						return keys, sess.Find(&keys)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										16
									
								
								models/store.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								models/store.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,16 @@
 | 
				
			|||||||
 | 
					// Copyright 2021 The Gitea Authors. All rights reserved.
 | 
				
			||||||
 | 
					// Use of this source code is governed by a MIT-style
 | 
				
			||||||
 | 
					// license that can be found in the LICENSE file.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import "github.com/lafriks/xormstore"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// CreateStore creates a xormstore for the provided table and key
 | 
				
			||||||
 | 
					func CreateStore(table, key string) (*xormstore.Store, error) {
 | 
				
			||||||
 | 
						store, err := xormstore.NewOptions(x, xormstore.Options{
 | 
				
			||||||
 | 
							TableName: table,
 | 
				
			||||||
 | 
						}, []byte(key))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return store, err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										341
									
								
								models/user.go
									
									
									
									
									
								
							
							
						
						
									
										341
									
								
								models/user.go
									
									
									
									
									
								
							@@ -34,7 +34,6 @@ import (
 | 
				
			|||||||
	"golang.org/x/crypto/bcrypt"
 | 
						"golang.org/x/crypto/bcrypt"
 | 
				
			||||||
	"golang.org/x/crypto/pbkdf2"
 | 
						"golang.org/x/crypto/pbkdf2"
 | 
				
			||||||
	"golang.org/x/crypto/scrypt"
 | 
						"golang.org/x/crypto/scrypt"
 | 
				
			||||||
	"golang.org/x/crypto/ssh"
 | 
					 | 
				
			||||||
	"xorm.io/builder"
 | 
						"xorm.io/builder"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -1484,6 +1483,13 @@ func GetUserIDsByNames(names []string, ignoreNonExistent bool) ([]int64, error)
 | 
				
			|||||||
	return ids, nil
 | 
						return ids, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetUsersBySource returns a list of Users for a login source
 | 
				
			||||||
 | 
					func GetUsersBySource(s *LoginSource) ([]*User, error) {
 | 
				
			||||||
 | 
						var users []*User
 | 
				
			||||||
 | 
						err := x.Where("login_type = ? AND login_source = ?", s.Type, s.ID).Find(&users)
 | 
				
			||||||
 | 
						return users, err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// UserCommit represents a commit with validation of user.
 | 
					// UserCommit represents a commit with validation of user.
 | 
				
			||||||
type UserCommit struct {
 | 
					type UserCommit struct {
 | 
				
			||||||
	User *User
 | 
						User *User
 | 
				
			||||||
@@ -1724,339 +1730,6 @@ func GetWatchedRepos(userID int64, private bool, listOptions ListOptions) ([]*Re
 | 
				
			|||||||
	return repos, sess.Find(&repos)
 | 
						return repos, sess.Find(&repos)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// deleteKeysMarkedForDeletion returns true if ssh keys needs update
 | 
					 | 
				
			||||||
func deleteKeysMarkedForDeletion(keys []string) (bool, error) {
 | 
					 | 
				
			||||||
	// Start session
 | 
					 | 
				
			||||||
	sess := x.NewSession()
 | 
					 | 
				
			||||||
	defer sess.Close()
 | 
					 | 
				
			||||||
	if err := sess.Begin(); err != nil {
 | 
					 | 
				
			||||||
		return false, err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Delete keys marked for deletion
 | 
					 | 
				
			||||||
	var sshKeysNeedUpdate bool
 | 
					 | 
				
			||||||
	for _, KeyToDelete := range keys {
 | 
					 | 
				
			||||||
		key, err := searchPublicKeyByContentWithEngine(sess, KeyToDelete)
 | 
					 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			log.Error("SearchPublicKeyByContent: %v", err)
 | 
					 | 
				
			||||||
			continue
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		if err = deletePublicKeys(sess, key.ID); err != nil {
 | 
					 | 
				
			||||||
			log.Error("deletePublicKeys: %v", err)
 | 
					 | 
				
			||||||
			continue
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		sshKeysNeedUpdate = true
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if err := sess.Commit(); err != nil {
 | 
					 | 
				
			||||||
		return false, err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return sshKeysNeedUpdate, nil
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// addLdapSSHPublicKeys add a users public keys. Returns true if there are changes.
 | 
					 | 
				
			||||||
func addLdapSSHPublicKeys(usr *User, s *LoginSource, sshPublicKeys []string) bool {
 | 
					 | 
				
			||||||
	var sshKeysNeedUpdate bool
 | 
					 | 
				
			||||||
	for _, sshKey := range sshPublicKeys {
 | 
					 | 
				
			||||||
		var err error
 | 
					 | 
				
			||||||
		found := false
 | 
					 | 
				
			||||||
		keys := []byte(sshKey)
 | 
					 | 
				
			||||||
	loop:
 | 
					 | 
				
			||||||
		for len(keys) > 0 && err == nil {
 | 
					 | 
				
			||||||
			var out ssh.PublicKey
 | 
					 | 
				
			||||||
			// We ignore options as they are not relevant to Gitea
 | 
					 | 
				
			||||||
			out, _, _, keys, err = ssh.ParseAuthorizedKey(keys)
 | 
					 | 
				
			||||||
			if err != nil {
 | 
					 | 
				
			||||||
				break loop
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			found = true
 | 
					 | 
				
			||||||
			marshalled := string(ssh.MarshalAuthorizedKey(out))
 | 
					 | 
				
			||||||
			marshalled = marshalled[:len(marshalled)-1]
 | 
					 | 
				
			||||||
			sshKeyName := fmt.Sprintf("%s-%s", s.Name, ssh.FingerprintSHA256(out))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			if _, err := AddPublicKey(usr.ID, sshKeyName, marshalled, s.ID); err != nil {
 | 
					 | 
				
			||||||
				if IsErrKeyAlreadyExist(err) {
 | 
					 | 
				
			||||||
					log.Trace("addLdapSSHPublicKeys[%s]: LDAP Public SSH Key %s already exists for user", sshKeyName, usr.Name)
 | 
					 | 
				
			||||||
				} else {
 | 
					 | 
				
			||||||
					log.Error("addLdapSSHPublicKeys[%s]: Error adding LDAP Public SSH Key for user %s: %v", sshKeyName, usr.Name, err)
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			} else {
 | 
					 | 
				
			||||||
				log.Trace("addLdapSSHPublicKeys[%s]: Added LDAP Public SSH Key for user %s", sshKeyName, usr.Name)
 | 
					 | 
				
			||||||
				sshKeysNeedUpdate = true
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		if !found && err != nil {
 | 
					 | 
				
			||||||
			log.Warn("addLdapSSHPublicKeys[%s]: Skipping invalid LDAP Public SSH Key for user %s: %v", s.Name, usr.Name, sshKey)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return sshKeysNeedUpdate
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// synchronizeLdapSSHPublicKeys updates a users public keys. Returns true if there are changes.
 | 
					 | 
				
			||||||
func synchronizeLdapSSHPublicKeys(usr *User, s *LoginSource, sshPublicKeys []string) bool {
 | 
					 | 
				
			||||||
	var sshKeysNeedUpdate bool
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	log.Trace("synchronizeLdapSSHPublicKeys[%s]: Handling LDAP Public SSH Key synchronization for user %s", s.Name, usr.Name)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Get Public Keys from DB with current LDAP source
 | 
					 | 
				
			||||||
	var giteaKeys []string
 | 
					 | 
				
			||||||
	keys, err := ListPublicLdapSSHKeys(usr.ID, s.ID)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		log.Error("synchronizeLdapSSHPublicKeys[%s]: Error listing LDAP Public SSH Keys for user %s: %v", s.Name, usr.Name, err)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	for _, v := range keys {
 | 
					 | 
				
			||||||
		giteaKeys = append(giteaKeys, v.OmitEmail())
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Get Public Keys from LDAP and skip duplicate keys
 | 
					 | 
				
			||||||
	var ldapKeys []string
 | 
					 | 
				
			||||||
	for _, v := range sshPublicKeys {
 | 
					 | 
				
			||||||
		sshKeySplit := strings.Split(v, " ")
 | 
					 | 
				
			||||||
		if len(sshKeySplit) > 1 {
 | 
					 | 
				
			||||||
			ldapKey := strings.Join(sshKeySplit[:2], " ")
 | 
					 | 
				
			||||||
			if !util.ExistsInSlice(ldapKey, ldapKeys) {
 | 
					 | 
				
			||||||
				ldapKeys = append(ldapKeys, ldapKey)
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Check if Public Key sync is needed
 | 
					 | 
				
			||||||
	if util.IsEqualSlice(giteaKeys, ldapKeys) {
 | 
					 | 
				
			||||||
		log.Trace("synchronizeLdapSSHPublicKeys[%s]: LDAP Public Keys are already in sync for %s (LDAP:%v/DB:%v)", s.Name, usr.Name, len(ldapKeys), len(giteaKeys))
 | 
					 | 
				
			||||||
		return false
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	log.Trace("synchronizeLdapSSHPublicKeys[%s]: LDAP Public Key needs update for user %s (LDAP:%v/DB:%v)", s.Name, usr.Name, len(ldapKeys), len(giteaKeys))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Add LDAP Public SSH Keys that doesn't already exist in DB
 | 
					 | 
				
			||||||
	var newLdapSSHKeys []string
 | 
					 | 
				
			||||||
	for _, LDAPPublicSSHKey := range ldapKeys {
 | 
					 | 
				
			||||||
		if !util.ExistsInSlice(LDAPPublicSSHKey, giteaKeys) {
 | 
					 | 
				
			||||||
			newLdapSSHKeys = append(newLdapSSHKeys, LDAPPublicSSHKey)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	if addLdapSSHPublicKeys(usr, s, newLdapSSHKeys) {
 | 
					 | 
				
			||||||
		sshKeysNeedUpdate = true
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Mark LDAP keys from DB that doesn't exist in LDAP for deletion
 | 
					 | 
				
			||||||
	var giteaKeysToDelete []string
 | 
					 | 
				
			||||||
	for _, giteaKey := range giteaKeys {
 | 
					 | 
				
			||||||
		if !util.ExistsInSlice(giteaKey, ldapKeys) {
 | 
					 | 
				
			||||||
			log.Trace("synchronizeLdapSSHPublicKeys[%s]: Marking LDAP Public SSH Key for deletion for user %s: %v", s.Name, usr.Name, giteaKey)
 | 
					 | 
				
			||||||
			giteaKeysToDelete = append(giteaKeysToDelete, giteaKey)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Delete LDAP keys from DB that doesn't exist in LDAP
 | 
					 | 
				
			||||||
	needUpd, err := deleteKeysMarkedForDeletion(giteaKeysToDelete)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		log.Error("synchronizeLdapSSHPublicKeys[%s]: Error deleting LDAP Public SSH Keys marked for deletion for user %s: %v", s.Name, usr.Name, err)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	if needUpd {
 | 
					 | 
				
			||||||
		sshKeysNeedUpdate = true
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return sshKeysNeedUpdate
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// SyncExternalUsers is used to synchronize users with external authorization source
 | 
					 | 
				
			||||||
func SyncExternalUsers(ctx context.Context, updateExisting bool) error {
 | 
					 | 
				
			||||||
	log.Trace("Doing: SyncExternalUsers")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	ls, err := LoginSources()
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		log.Error("SyncExternalUsers: %v", err)
 | 
					 | 
				
			||||||
		return err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	for _, s := range ls {
 | 
					 | 
				
			||||||
		if !s.IsActived || !s.IsSyncEnabled {
 | 
					 | 
				
			||||||
			continue
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		select {
 | 
					 | 
				
			||||||
		case <-ctx.Done():
 | 
					 | 
				
			||||||
			log.Warn("SyncExternalUsers: Cancelled before update of %s", s.Name)
 | 
					 | 
				
			||||||
			return ErrCancelledf("Before update of %s", s.Name)
 | 
					 | 
				
			||||||
		default:
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if s.IsLDAP() {
 | 
					 | 
				
			||||||
			log.Trace("Doing: SyncExternalUsers[%s]", s.Name)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			var existingUsers []int64
 | 
					 | 
				
			||||||
			isAttributeSSHPublicKeySet := len(strings.TrimSpace(s.LDAP().AttributeSSHPublicKey)) > 0
 | 
					 | 
				
			||||||
			var sshKeysNeedUpdate bool
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			// Find all users with this login type
 | 
					 | 
				
			||||||
			var users []*User
 | 
					 | 
				
			||||||
			err = x.Where("login_type = ?", LoginLDAP).
 | 
					 | 
				
			||||||
				And("login_source = ?", s.ID).
 | 
					 | 
				
			||||||
				Find(&users)
 | 
					 | 
				
			||||||
			if err != nil {
 | 
					 | 
				
			||||||
				log.Error("SyncExternalUsers: %v", err)
 | 
					 | 
				
			||||||
				return err
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			select {
 | 
					 | 
				
			||||||
			case <-ctx.Done():
 | 
					 | 
				
			||||||
				log.Warn("SyncExternalUsers: Cancelled before update of %s", s.Name)
 | 
					 | 
				
			||||||
				return ErrCancelledf("Before update of %s", s.Name)
 | 
					 | 
				
			||||||
			default:
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			sr, err := s.LDAP().SearchEntries()
 | 
					 | 
				
			||||||
			if err != nil {
 | 
					 | 
				
			||||||
				log.Error("SyncExternalUsers LDAP source failure [%s], skipped", s.Name)
 | 
					 | 
				
			||||||
				continue
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			if len(sr) == 0 {
 | 
					 | 
				
			||||||
				if !s.LDAP().AllowDeactivateAll {
 | 
					 | 
				
			||||||
					log.Error("LDAP search found no entries but did not report an error. Refusing to deactivate all users")
 | 
					 | 
				
			||||||
					continue
 | 
					 | 
				
			||||||
				} else {
 | 
					 | 
				
			||||||
					log.Warn("LDAP search found no entries but did not report an error. All users will be deactivated as per settings")
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			for _, su := range sr {
 | 
					 | 
				
			||||||
				select {
 | 
					 | 
				
			||||||
				case <-ctx.Done():
 | 
					 | 
				
			||||||
					log.Warn("SyncExternalUsers: Cancelled at update of %s before completed update of users", s.Name)
 | 
					 | 
				
			||||||
					// Rewrite authorized_keys file if LDAP Public SSH Key attribute is set and any key was added or removed
 | 
					 | 
				
			||||||
					if sshKeysNeedUpdate {
 | 
					 | 
				
			||||||
						err = RewriteAllPublicKeys()
 | 
					 | 
				
			||||||
						if err != nil {
 | 
					 | 
				
			||||||
							log.Error("RewriteAllPublicKeys: %v", err)
 | 
					 | 
				
			||||||
						}
 | 
					 | 
				
			||||||
					}
 | 
					 | 
				
			||||||
					return ErrCancelledf("During update of %s before completed update of users", s.Name)
 | 
					 | 
				
			||||||
				default:
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
				if len(su.Username) == 0 {
 | 
					 | 
				
			||||||
					continue
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				if len(su.Mail) == 0 {
 | 
					 | 
				
			||||||
					su.Mail = fmt.Sprintf("%s@localhost", su.Username)
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				var usr *User
 | 
					 | 
				
			||||||
				// Search for existing user
 | 
					 | 
				
			||||||
				for _, du := range users {
 | 
					 | 
				
			||||||
					if du.LowerName == strings.ToLower(su.Username) {
 | 
					 | 
				
			||||||
						usr = du
 | 
					 | 
				
			||||||
						break
 | 
					 | 
				
			||||||
					}
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				fullName := composeFullName(su.Name, su.Surname, su.Username)
 | 
					 | 
				
			||||||
				// If no existing user found, create one
 | 
					 | 
				
			||||||
				if usr == nil {
 | 
					 | 
				
			||||||
					log.Trace("SyncExternalUsers[%s]: Creating user %s", s.Name, su.Username)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
					usr = &User{
 | 
					 | 
				
			||||||
						LowerName:    strings.ToLower(su.Username),
 | 
					 | 
				
			||||||
						Name:         su.Username,
 | 
					 | 
				
			||||||
						FullName:     fullName,
 | 
					 | 
				
			||||||
						LoginType:    s.Type,
 | 
					 | 
				
			||||||
						LoginSource:  s.ID,
 | 
					 | 
				
			||||||
						LoginName:    su.Username,
 | 
					 | 
				
			||||||
						Email:        su.Mail,
 | 
					 | 
				
			||||||
						IsAdmin:      su.IsAdmin,
 | 
					 | 
				
			||||||
						IsRestricted: su.IsRestricted,
 | 
					 | 
				
			||||||
						IsActive:     true,
 | 
					 | 
				
			||||||
					}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
					err = CreateUser(usr)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
					if err != nil {
 | 
					 | 
				
			||||||
						log.Error("SyncExternalUsers[%s]: Error creating user %s: %v", s.Name, su.Username, err)
 | 
					 | 
				
			||||||
					} else if isAttributeSSHPublicKeySet {
 | 
					 | 
				
			||||||
						log.Trace("SyncExternalUsers[%s]: Adding LDAP Public SSH Keys for user %s", s.Name, usr.Name)
 | 
					 | 
				
			||||||
						if addLdapSSHPublicKeys(usr, s, su.SSHPublicKey) {
 | 
					 | 
				
			||||||
							sshKeysNeedUpdate = true
 | 
					 | 
				
			||||||
						}
 | 
					 | 
				
			||||||
					}
 | 
					 | 
				
			||||||
				} else if updateExisting {
 | 
					 | 
				
			||||||
					existingUsers = append(existingUsers, usr.ID)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
					// Synchronize SSH Public Key if that attribute is set
 | 
					 | 
				
			||||||
					if isAttributeSSHPublicKeySet && synchronizeLdapSSHPublicKeys(usr, s, su.SSHPublicKey) {
 | 
					 | 
				
			||||||
						sshKeysNeedUpdate = true
 | 
					 | 
				
			||||||
					}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
					// Check if user data has changed
 | 
					 | 
				
			||||||
					if (len(s.LDAP().AdminFilter) > 0 && usr.IsAdmin != su.IsAdmin) ||
 | 
					 | 
				
			||||||
						(len(s.LDAP().RestrictedFilter) > 0 && usr.IsRestricted != su.IsRestricted) ||
 | 
					 | 
				
			||||||
						!strings.EqualFold(usr.Email, su.Mail) ||
 | 
					 | 
				
			||||||
						usr.FullName != fullName ||
 | 
					 | 
				
			||||||
						!usr.IsActive {
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
						log.Trace("SyncExternalUsers[%s]: Updating user %s", s.Name, usr.Name)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
						usr.FullName = fullName
 | 
					 | 
				
			||||||
						usr.Email = su.Mail
 | 
					 | 
				
			||||||
						// Change existing admin flag only if AdminFilter option is set
 | 
					 | 
				
			||||||
						if len(s.LDAP().AdminFilter) > 0 {
 | 
					 | 
				
			||||||
							usr.IsAdmin = su.IsAdmin
 | 
					 | 
				
			||||||
						}
 | 
					 | 
				
			||||||
						// Change existing restricted flag only if RestrictedFilter option is set
 | 
					 | 
				
			||||||
						if !usr.IsAdmin && len(s.LDAP().RestrictedFilter) > 0 {
 | 
					 | 
				
			||||||
							usr.IsRestricted = su.IsRestricted
 | 
					 | 
				
			||||||
						}
 | 
					 | 
				
			||||||
						usr.IsActive = true
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
						err = UpdateUserCols(usr, "full_name", "email", "is_admin", "is_restricted", "is_active")
 | 
					 | 
				
			||||||
						if err != nil {
 | 
					 | 
				
			||||||
							log.Error("SyncExternalUsers[%s]: Error updating user %s: %v", s.Name, usr.Name, err)
 | 
					 | 
				
			||||||
						}
 | 
					 | 
				
			||||||
					}
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			// Rewrite authorized_keys file if LDAP Public SSH Key attribute is set and any key was added or removed
 | 
					 | 
				
			||||||
			if sshKeysNeedUpdate {
 | 
					 | 
				
			||||||
				err = RewriteAllPublicKeys()
 | 
					 | 
				
			||||||
				if err != nil {
 | 
					 | 
				
			||||||
					log.Error("RewriteAllPublicKeys: %v", err)
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			select {
 | 
					 | 
				
			||||||
			case <-ctx.Done():
 | 
					 | 
				
			||||||
				log.Warn("SyncExternalUsers: Cancelled during update of %s before delete users", s.Name)
 | 
					 | 
				
			||||||
				return ErrCancelledf("During update of %s before delete users", s.Name)
 | 
					 | 
				
			||||||
			default:
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			// Deactivate users not present in LDAP
 | 
					 | 
				
			||||||
			if updateExisting {
 | 
					 | 
				
			||||||
				for _, usr := range users {
 | 
					 | 
				
			||||||
					found := false
 | 
					 | 
				
			||||||
					for _, uid := range existingUsers {
 | 
					 | 
				
			||||||
						if usr.ID == uid {
 | 
					 | 
				
			||||||
							found = true
 | 
					 | 
				
			||||||
							break
 | 
					 | 
				
			||||||
						}
 | 
					 | 
				
			||||||
					}
 | 
					 | 
				
			||||||
					if !found {
 | 
					 | 
				
			||||||
						log.Trace("SyncExternalUsers[%s]: Deactivating user %s", s.Name, usr.Name)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
						usr.IsActive = false
 | 
					 | 
				
			||||||
						err = UpdateUserCols(usr, "is_active")
 | 
					 | 
				
			||||||
						if err != nil {
 | 
					 | 
				
			||||||
							log.Error("SyncExternalUsers[%s]: Error deactivating user %s: %v", s.Name, usr.Name, err)
 | 
					 | 
				
			||||||
						}
 | 
					 | 
				
			||||||
					}
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return nil
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// IterateUser iterate users
 | 
					// IterateUser iterate users
 | 
				
			||||||
func IterateUser(f func(user *User) error) error {
 | 
					func IterateUser(f func(user *User) error) error {
 | 
				
			||||||
	var start int
 | 
						var start int
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -453,8 +453,8 @@ ssh-dss AAAAB3NzaC1kc3MAAACBAOChCC7lf6Uo9n7BmZ6M8St19PZf4Tn59NriyboW2x/DZuYAz3ib
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	for i, kase := range testCases {
 | 
						for i, kase := range testCases {
 | 
				
			||||||
		s.ID = int64(i) + 20
 | 
							s.ID = int64(i) + 20
 | 
				
			||||||
		addLdapSSHPublicKeys(user, s, []string{kase.keyString})
 | 
							AddPublicKeysBySource(user, s, []string{kase.keyString})
 | 
				
			||||||
		keys, err := ListPublicLdapSSHKeys(user.ID, s.ID)
 | 
							keys, err := ListPublicKeysBySource(user.ID, s.ID)
 | 
				
			||||||
		assert.NoError(t, err)
 | 
							assert.NoError(t, err)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			continue
 | 
								continue
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -218,7 +218,7 @@ func (ctx *APIContext) CheckForOTP() {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// APIAuth converts auth.Auth as a middleware
 | 
					// APIAuth converts auth.Auth as a middleware
 | 
				
			||||||
func APIAuth(authMethod auth.Auth) func(*APIContext) {
 | 
					func APIAuth(authMethod auth.Method) func(*APIContext) {
 | 
				
			||||||
	return func(ctx *APIContext) {
 | 
						return func(ctx *APIContext) {
 | 
				
			||||||
		// Get user from session if logged in.
 | 
							// Get user from session if logged in.
 | 
				
			||||||
		ctx.User = authMethod.Verify(ctx.Req, ctx.Resp, ctx, ctx.Session)
 | 
							ctx.User = authMethod.Verify(ctx.Req, ctx.Resp, ctx, ctx.Session)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -627,7 +627,7 @@ func getCsrfOpts() CsrfOptions {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Auth converts auth.Auth as a middleware
 | 
					// Auth converts auth.Auth as a middleware
 | 
				
			||||||
func Auth(authMethod auth.Auth) func(*Context) {
 | 
					func Auth(authMethod auth.Method) func(*Context) {
 | 
				
			||||||
	return func(ctx *Context) {
 | 
						return func(ctx *Context) {
 | 
				
			||||||
		ctx.User = authMethod.Verify(ctx.Req, ctx.Resp, ctx, ctx.Session)
 | 
							ctx.User = authMethod.Verify(ctx.Req, ctx.Resp, ctx, ctx.Session)
 | 
				
			||||||
		if ctx.User != nil {
 | 
							if ctx.User != nil {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -12,6 +12,7 @@ import (
 | 
				
			|||||||
	"code.gitea.io/gitea/modules/migrations"
 | 
						"code.gitea.io/gitea/modules/migrations"
 | 
				
			||||||
	repository_service "code.gitea.io/gitea/modules/repository"
 | 
						repository_service "code.gitea.io/gitea/modules/repository"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/setting"
 | 
						"code.gitea.io/gitea/modules/setting"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/services/auth"
 | 
				
			||||||
	mirror_service "code.gitea.io/gitea/services/mirror"
 | 
						mirror_service "code.gitea.io/gitea/services/mirror"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -80,7 +81,7 @@ func registerSyncExternalUsers() {
 | 
				
			|||||||
		UpdateExisting: true,
 | 
							UpdateExisting: true,
 | 
				
			||||||
	}, func(ctx context.Context, _ *models.User, config Config) error {
 | 
						}, func(ctx context.Context, _ *models.User, config Config) error {
 | 
				
			||||||
		realConfig := config.(*UpdateExistingConfig)
 | 
							realConfig := config.(*UpdateExistingConfig)
 | 
				
			||||||
		return models.SyncExternalUsers(ctx, realConfig.UpdateExisting)
 | 
							return auth.SyncExternalUsers(ctx, realConfig.UpdateExisting)
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -35,6 +35,7 @@ import (
 | 
				
			|||||||
	web_routers "code.gitea.io/gitea/routers/web"
 | 
						web_routers "code.gitea.io/gitea/routers/web"
 | 
				
			||||||
	"code.gitea.io/gitea/services/archiver"
 | 
						"code.gitea.io/gitea/services/archiver"
 | 
				
			||||||
	"code.gitea.io/gitea/services/auth"
 | 
						"code.gitea.io/gitea/services/auth"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/services/auth/source/oauth2"
 | 
				
			||||||
	"code.gitea.io/gitea/services/mailer"
 | 
						"code.gitea.io/gitea/services/mailer"
 | 
				
			||||||
	mirror_service "code.gitea.io/gitea/services/mirror"
 | 
						mirror_service "code.gitea.io/gitea/services/mirror"
 | 
				
			||||||
	pull_service "code.gitea.io/gitea/services/pull"
 | 
						pull_service "code.gitea.io/gitea/services/pull"
 | 
				
			||||||
@@ -100,7 +101,7 @@ func GlobalInit(ctx context.Context) {
 | 
				
			|||||||
		log.Fatal("ORM engine initialization failed: %v", err)
 | 
							log.Fatal("ORM engine initialization failed: %v", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err := models.InitOAuth2(); err != nil {
 | 
						if err := oauth2.Init(); err != nil {
 | 
				
			||||||
		log.Fatal("Failed to initialize OAuth2 support: %v", err)
 | 
							log.Fatal("Failed to initialize OAuth2 support: %v", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -11,8 +11,6 @@ import (
 | 
				
			|||||||
	"regexp"
 | 
						"regexp"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"code.gitea.io/gitea/models"
 | 
						"code.gitea.io/gitea/models"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/auth/ldap"
 | 
					 | 
				
			||||||
	"code.gitea.io/gitea/modules/auth/oauth2"
 | 
					 | 
				
			||||||
	"code.gitea.io/gitea/modules/auth/pam"
 | 
						"code.gitea.io/gitea/modules/auth/pam"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/base"
 | 
						"code.gitea.io/gitea/modules/base"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/context"
 | 
						"code.gitea.io/gitea/modules/context"
 | 
				
			||||||
@@ -20,6 +18,11 @@ import (
 | 
				
			|||||||
	"code.gitea.io/gitea/modules/setting"
 | 
						"code.gitea.io/gitea/modules/setting"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/util"
 | 
						"code.gitea.io/gitea/modules/util"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/web"
 | 
						"code.gitea.io/gitea/modules/web"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/services/auth/source/ldap"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/services/auth/source/oauth2"
 | 
				
			||||||
 | 
						pamService "code.gitea.io/gitea/services/auth/source/pam"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/services/auth/source/smtp"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/services/auth/source/sspi"
 | 
				
			||||||
	"code.gitea.io/gitea/services/forms"
 | 
						"code.gitea.io/gitea/services/forms"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"xorm.io/xorm/convert"
 | 
						"xorm.io/xorm/convert"
 | 
				
			||||||
@@ -74,9 +77,9 @@ var (
 | 
				
			|||||||
	}()
 | 
						}()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	securityProtocols = []dropdownItem{
 | 
						securityProtocols = []dropdownItem{
 | 
				
			||||||
		{models.SecurityProtocolNames[ldap.SecurityProtocolUnencrypted], ldap.SecurityProtocolUnencrypted},
 | 
							{ldap.SecurityProtocolNames[ldap.SecurityProtocolUnencrypted], ldap.SecurityProtocolUnencrypted},
 | 
				
			||||||
		{models.SecurityProtocolNames[ldap.SecurityProtocolLDAPS], ldap.SecurityProtocolLDAPS},
 | 
							{ldap.SecurityProtocolNames[ldap.SecurityProtocolLDAPS], ldap.SecurityProtocolLDAPS},
 | 
				
			||||||
		{models.SecurityProtocolNames[ldap.SecurityProtocolStartTLS], ldap.SecurityProtocolStartTLS},
 | 
							{ldap.SecurityProtocolNames[ldap.SecurityProtocolStartTLS], ldap.SecurityProtocolStartTLS},
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -88,15 +91,15 @@ func NewAuthSource(ctx *context.Context) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	ctx.Data["type"] = models.LoginLDAP
 | 
						ctx.Data["type"] = models.LoginLDAP
 | 
				
			||||||
	ctx.Data["CurrentTypeName"] = models.LoginNames[models.LoginLDAP]
 | 
						ctx.Data["CurrentTypeName"] = models.LoginNames[models.LoginLDAP]
 | 
				
			||||||
	ctx.Data["CurrentSecurityProtocol"] = models.SecurityProtocolNames[ldap.SecurityProtocolUnencrypted]
 | 
						ctx.Data["CurrentSecurityProtocol"] = ldap.SecurityProtocolNames[ldap.SecurityProtocolUnencrypted]
 | 
				
			||||||
	ctx.Data["smtp_auth"] = "PLAIN"
 | 
						ctx.Data["smtp_auth"] = "PLAIN"
 | 
				
			||||||
	ctx.Data["is_active"] = true
 | 
						ctx.Data["is_active"] = true
 | 
				
			||||||
	ctx.Data["is_sync_enabled"] = true
 | 
						ctx.Data["is_sync_enabled"] = true
 | 
				
			||||||
	ctx.Data["AuthSources"] = authSources
 | 
						ctx.Data["AuthSources"] = authSources
 | 
				
			||||||
	ctx.Data["SecurityProtocols"] = securityProtocols
 | 
						ctx.Data["SecurityProtocols"] = securityProtocols
 | 
				
			||||||
	ctx.Data["SMTPAuths"] = models.SMTPAuths
 | 
						ctx.Data["SMTPAuths"] = smtp.Authenticators
 | 
				
			||||||
	ctx.Data["OAuth2Providers"] = models.OAuth2Providers
 | 
						ctx.Data["OAuth2Providers"] = oauth2.Providers
 | 
				
			||||||
	ctx.Data["OAuth2DefaultCustomURLMappings"] = models.OAuth2DefaultCustomURLMappings
 | 
						ctx.Data["OAuth2DefaultCustomURLMappings"] = oauth2.DefaultCustomURLMappings
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	ctx.Data["SSPIAutoCreateUsers"] = true
 | 
						ctx.Data["SSPIAutoCreateUsers"] = true
 | 
				
			||||||
	ctx.Data["SSPIAutoActivateUsers"] = true
 | 
						ctx.Data["SSPIAutoActivateUsers"] = true
 | 
				
			||||||
@@ -105,7 +108,7 @@ func NewAuthSource(ctx *context.Context) {
 | 
				
			|||||||
	ctx.Data["SSPIDefaultLanguage"] = ""
 | 
						ctx.Data["SSPIDefaultLanguage"] = ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// only the first as default
 | 
						// only the first as default
 | 
				
			||||||
	for key := range models.OAuth2Providers {
 | 
						for key := range oauth2.Providers {
 | 
				
			||||||
		ctx.Data["oauth2_provider"] = key
 | 
							ctx.Data["oauth2_provider"] = key
 | 
				
			||||||
		break
 | 
							break
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -113,13 +116,12 @@ func NewAuthSource(ctx *context.Context) {
 | 
				
			|||||||
	ctx.HTML(http.StatusOK, tplAuthNew)
 | 
						ctx.HTML(http.StatusOK, tplAuthNew)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func parseLDAPConfig(form forms.AuthenticationForm) *models.LDAPConfig {
 | 
					func parseLDAPConfig(form forms.AuthenticationForm) *ldap.Source {
 | 
				
			||||||
	var pageSize uint32
 | 
						var pageSize uint32
 | 
				
			||||||
	if form.UsePagedSearch {
 | 
						if form.UsePagedSearch {
 | 
				
			||||||
		pageSize = uint32(form.SearchPageSize)
 | 
							pageSize = uint32(form.SearchPageSize)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	return &models.LDAPConfig{
 | 
						return &ldap.Source{
 | 
				
			||||||
		Source: &ldap.Source{
 | 
					 | 
				
			||||||
		Name:                  form.Name,
 | 
							Name:                  form.Name,
 | 
				
			||||||
		Host:                  form.Host,
 | 
							Host:                  form.Host,
 | 
				
			||||||
		Port:                  form.Port,
 | 
							Port:                  form.Port,
 | 
				
			||||||
@@ -146,12 +148,11 @@ func parseLDAPConfig(form forms.AuthenticationForm) *models.LDAPConfig {
 | 
				
			|||||||
		RestrictedFilter:      form.RestrictedFilter,
 | 
							RestrictedFilter:      form.RestrictedFilter,
 | 
				
			||||||
		AllowDeactivateAll:    form.AllowDeactivateAll,
 | 
							AllowDeactivateAll:    form.AllowDeactivateAll,
 | 
				
			||||||
		Enabled:               true,
 | 
							Enabled:               true,
 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func parseSMTPConfig(form forms.AuthenticationForm) *models.SMTPConfig {
 | 
					func parseSMTPConfig(form forms.AuthenticationForm) *smtp.Source {
 | 
				
			||||||
	return &models.SMTPConfig{
 | 
						return &smtp.Source{
 | 
				
			||||||
		Auth:           form.SMTPAuth,
 | 
							Auth:           form.SMTPAuth,
 | 
				
			||||||
		Host:           form.SMTPHost,
 | 
							Host:           form.SMTPHost,
 | 
				
			||||||
		Port:           form.SMTPPort,
 | 
							Port:           form.SMTPPort,
 | 
				
			||||||
@@ -161,7 +162,7 @@ func parseSMTPConfig(form forms.AuthenticationForm) *models.SMTPConfig {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func parseOAuth2Config(form forms.AuthenticationForm) *models.OAuth2Config {
 | 
					func parseOAuth2Config(form forms.AuthenticationForm) *oauth2.Source {
 | 
				
			||||||
	var customURLMapping *oauth2.CustomURLMapping
 | 
						var customURLMapping *oauth2.CustomURLMapping
 | 
				
			||||||
	if form.Oauth2UseCustomURL {
 | 
						if form.Oauth2UseCustomURL {
 | 
				
			||||||
		customURLMapping = &oauth2.CustomURLMapping{
 | 
							customURLMapping = &oauth2.CustomURLMapping{
 | 
				
			||||||
@@ -173,7 +174,7 @@ func parseOAuth2Config(form forms.AuthenticationForm) *models.OAuth2Config {
 | 
				
			|||||||
	} else {
 | 
						} else {
 | 
				
			||||||
		customURLMapping = nil
 | 
							customURLMapping = nil
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	return &models.OAuth2Config{
 | 
						return &oauth2.Source{
 | 
				
			||||||
		Provider:                      form.Oauth2Provider,
 | 
							Provider:                      form.Oauth2Provider,
 | 
				
			||||||
		ClientID:                      form.Oauth2Key,
 | 
							ClientID:                      form.Oauth2Key,
 | 
				
			||||||
		ClientSecret:                  form.Oauth2Secret,
 | 
							ClientSecret:                  form.Oauth2Secret,
 | 
				
			||||||
@@ -183,7 +184,7 @@ func parseOAuth2Config(form forms.AuthenticationForm) *models.OAuth2Config {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func parseSSPIConfig(ctx *context.Context, form forms.AuthenticationForm) (*models.SSPIConfig, error) {
 | 
					func parseSSPIConfig(ctx *context.Context, form forms.AuthenticationForm) (*sspi.Source, error) {
 | 
				
			||||||
	if util.IsEmptyString(form.SSPISeparatorReplacement) {
 | 
						if util.IsEmptyString(form.SSPISeparatorReplacement) {
 | 
				
			||||||
		ctx.Data["Err_SSPISeparatorReplacement"] = true
 | 
							ctx.Data["Err_SSPISeparatorReplacement"] = true
 | 
				
			||||||
		return nil, errors.New(ctx.Tr("form.SSPISeparatorReplacement") + ctx.Tr("form.require_error"))
 | 
							return nil, errors.New(ctx.Tr("form.SSPISeparatorReplacement") + ctx.Tr("form.require_error"))
 | 
				
			||||||
@@ -198,7 +199,7 @@ func parseSSPIConfig(ctx *context.Context, form forms.AuthenticationForm) (*mode
 | 
				
			|||||||
		return nil, errors.New(ctx.Tr("form.lang_select_error"))
 | 
							return nil, errors.New(ctx.Tr("form.lang_select_error"))
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return &models.SSPIConfig{
 | 
						return &sspi.Source{
 | 
				
			||||||
		AutoCreateUsers:      form.SSPIAutoCreateUsers,
 | 
							AutoCreateUsers:      form.SSPIAutoCreateUsers,
 | 
				
			||||||
		AutoActivateUsers:    form.SSPIAutoActivateUsers,
 | 
							AutoActivateUsers:    form.SSPIAutoActivateUsers,
 | 
				
			||||||
		StripDomainNames:     form.SSPIStripDomainNames,
 | 
							StripDomainNames:     form.SSPIStripDomainNames,
 | 
				
			||||||
@@ -215,12 +216,12 @@ func NewAuthSourcePost(ctx *context.Context) {
 | 
				
			|||||||
	ctx.Data["PageIsAdminAuthentications"] = true
 | 
						ctx.Data["PageIsAdminAuthentications"] = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	ctx.Data["CurrentTypeName"] = models.LoginNames[models.LoginType(form.Type)]
 | 
						ctx.Data["CurrentTypeName"] = models.LoginNames[models.LoginType(form.Type)]
 | 
				
			||||||
	ctx.Data["CurrentSecurityProtocol"] = models.SecurityProtocolNames[ldap.SecurityProtocol(form.SecurityProtocol)]
 | 
						ctx.Data["CurrentSecurityProtocol"] = ldap.SecurityProtocolNames[ldap.SecurityProtocol(form.SecurityProtocol)]
 | 
				
			||||||
	ctx.Data["AuthSources"] = authSources
 | 
						ctx.Data["AuthSources"] = authSources
 | 
				
			||||||
	ctx.Data["SecurityProtocols"] = securityProtocols
 | 
						ctx.Data["SecurityProtocols"] = securityProtocols
 | 
				
			||||||
	ctx.Data["SMTPAuths"] = models.SMTPAuths
 | 
						ctx.Data["SMTPAuths"] = smtp.Authenticators
 | 
				
			||||||
	ctx.Data["OAuth2Providers"] = models.OAuth2Providers
 | 
						ctx.Data["OAuth2Providers"] = oauth2.Providers
 | 
				
			||||||
	ctx.Data["OAuth2DefaultCustomURLMappings"] = models.OAuth2DefaultCustomURLMappings
 | 
						ctx.Data["OAuth2DefaultCustomURLMappings"] = oauth2.DefaultCustomURLMappings
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	ctx.Data["SSPIAutoCreateUsers"] = true
 | 
						ctx.Data["SSPIAutoCreateUsers"] = true
 | 
				
			||||||
	ctx.Data["SSPIAutoActivateUsers"] = true
 | 
						ctx.Data["SSPIAutoActivateUsers"] = true
 | 
				
			||||||
@@ -238,7 +239,7 @@ func NewAuthSourcePost(ctx *context.Context) {
 | 
				
			|||||||
		config = parseSMTPConfig(form)
 | 
							config = parseSMTPConfig(form)
 | 
				
			||||||
		hasTLS = true
 | 
							hasTLS = true
 | 
				
			||||||
	case models.LoginPAM:
 | 
						case models.LoginPAM:
 | 
				
			||||||
		config = &models.PAMConfig{
 | 
							config = &pamService.Source{
 | 
				
			||||||
			ServiceName: form.PAMServiceName,
 | 
								ServiceName: form.PAMServiceName,
 | 
				
			||||||
			EmailDomain: form.PAMEmailDomain,
 | 
								EmailDomain: form.PAMEmailDomain,
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@@ -271,7 +272,7 @@ func NewAuthSourcePost(ctx *context.Context) {
 | 
				
			|||||||
	if err := models.CreateLoginSource(&models.LoginSource{
 | 
						if err := models.CreateLoginSource(&models.LoginSource{
 | 
				
			||||||
		Type:          models.LoginType(form.Type),
 | 
							Type:          models.LoginType(form.Type),
 | 
				
			||||||
		Name:          form.Name,
 | 
							Name:          form.Name,
 | 
				
			||||||
		IsActived:     form.IsActive,
 | 
							IsActive:      form.IsActive,
 | 
				
			||||||
		IsSyncEnabled: form.IsSyncEnabled,
 | 
							IsSyncEnabled: form.IsSyncEnabled,
 | 
				
			||||||
		Cfg:           config,
 | 
							Cfg:           config,
 | 
				
			||||||
	}); err != nil {
 | 
						}); err != nil {
 | 
				
			||||||
@@ -297,9 +298,9 @@ func EditAuthSource(ctx *context.Context) {
 | 
				
			|||||||
	ctx.Data["PageIsAdminAuthentications"] = true
 | 
						ctx.Data["PageIsAdminAuthentications"] = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	ctx.Data["SecurityProtocols"] = securityProtocols
 | 
						ctx.Data["SecurityProtocols"] = securityProtocols
 | 
				
			||||||
	ctx.Data["SMTPAuths"] = models.SMTPAuths
 | 
						ctx.Data["SMTPAuths"] = smtp.Authenticators
 | 
				
			||||||
	ctx.Data["OAuth2Providers"] = models.OAuth2Providers
 | 
						ctx.Data["OAuth2Providers"] = oauth2.Providers
 | 
				
			||||||
	ctx.Data["OAuth2DefaultCustomURLMappings"] = models.OAuth2DefaultCustomURLMappings
 | 
						ctx.Data["OAuth2DefaultCustomURLMappings"] = oauth2.DefaultCustomURLMappings
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	source, err := models.GetLoginSourceByID(ctx.ParamsInt64(":authid"))
 | 
						source, err := models.GetLoginSourceByID(ctx.ParamsInt64(":authid"))
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
@@ -310,7 +311,7 @@ func EditAuthSource(ctx *context.Context) {
 | 
				
			|||||||
	ctx.Data["HasTLS"] = source.HasTLS()
 | 
						ctx.Data["HasTLS"] = source.HasTLS()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if source.IsOAuth2() {
 | 
						if source.IsOAuth2() {
 | 
				
			||||||
		ctx.Data["CurrentOAuth2Provider"] = models.OAuth2Providers[source.OAuth2().Provider]
 | 
							ctx.Data["CurrentOAuth2Provider"] = oauth2.Providers[source.Cfg.(*oauth2.Source).Provider]
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	ctx.HTML(http.StatusOK, tplAuthEdit)
 | 
						ctx.HTML(http.StatusOK, tplAuthEdit)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -322,9 +323,9 @@ func EditAuthSourcePost(ctx *context.Context) {
 | 
				
			|||||||
	ctx.Data["PageIsAdmin"] = true
 | 
						ctx.Data["PageIsAdmin"] = true
 | 
				
			||||||
	ctx.Data["PageIsAdminAuthentications"] = true
 | 
						ctx.Data["PageIsAdminAuthentications"] = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	ctx.Data["SMTPAuths"] = models.SMTPAuths
 | 
						ctx.Data["SMTPAuths"] = smtp.Authenticators
 | 
				
			||||||
	ctx.Data["OAuth2Providers"] = models.OAuth2Providers
 | 
						ctx.Data["OAuth2Providers"] = oauth2.Providers
 | 
				
			||||||
	ctx.Data["OAuth2DefaultCustomURLMappings"] = models.OAuth2DefaultCustomURLMappings
 | 
						ctx.Data["OAuth2DefaultCustomURLMappings"] = oauth2.DefaultCustomURLMappings
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	source, err := models.GetLoginSourceByID(ctx.ParamsInt64(":authid"))
 | 
						source, err := models.GetLoginSourceByID(ctx.ParamsInt64(":authid"))
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
@@ -346,7 +347,7 @@ func EditAuthSourcePost(ctx *context.Context) {
 | 
				
			|||||||
	case models.LoginSMTP:
 | 
						case models.LoginSMTP:
 | 
				
			||||||
		config = parseSMTPConfig(form)
 | 
							config = parseSMTPConfig(form)
 | 
				
			||||||
	case models.LoginPAM:
 | 
						case models.LoginPAM:
 | 
				
			||||||
		config = &models.PAMConfig{
 | 
							config = &pamService.Source{
 | 
				
			||||||
			ServiceName: form.PAMServiceName,
 | 
								ServiceName: form.PAMServiceName,
 | 
				
			||||||
			EmailDomain: form.PAMEmailDomain,
 | 
								EmailDomain: form.PAMEmailDomain,
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@@ -364,7 +365,7 @@ func EditAuthSourcePost(ctx *context.Context) {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	source.Name = form.Name
 | 
						source.Name = form.Name
 | 
				
			||||||
	source.IsActived = form.IsActive
 | 
						source.IsActive = form.IsActive
 | 
				
			||||||
	source.IsSyncEnabled = form.IsSyncEnabled
 | 
						source.IsSyncEnabled = form.IsSyncEnabled
 | 
				
			||||||
	source.Cfg = config
 | 
						source.Cfg = config
 | 
				
			||||||
	if err := models.UpdateSource(source); err != nil {
 | 
						if err := models.UpdateSource(source); err != nil {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,7 +14,6 @@ import (
 | 
				
			|||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"code.gitea.io/gitea/models"
 | 
						"code.gitea.io/gitea/models"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/auth/oauth2"
 | 
					 | 
				
			||||||
	"code.gitea.io/gitea/modules/base"
 | 
						"code.gitea.io/gitea/modules/base"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/context"
 | 
						"code.gitea.io/gitea/modules/context"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/eventsource"
 | 
						"code.gitea.io/gitea/modules/eventsource"
 | 
				
			||||||
@@ -27,6 +26,8 @@ import (
 | 
				
			|||||||
	"code.gitea.io/gitea/modules/web"
 | 
						"code.gitea.io/gitea/modules/web"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/web/middleware"
 | 
						"code.gitea.io/gitea/modules/web/middleware"
 | 
				
			||||||
	"code.gitea.io/gitea/routers/utils"
 | 
						"code.gitea.io/gitea/routers/utils"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/services/auth"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/services/auth/source/oauth2"
 | 
				
			||||||
	"code.gitea.io/gitea/services/externalaccount"
 | 
						"code.gitea.io/gitea/services/externalaccount"
 | 
				
			||||||
	"code.gitea.io/gitea/services/forms"
 | 
						"code.gitea.io/gitea/services/forms"
 | 
				
			||||||
	"code.gitea.io/gitea/services/mailer"
 | 
						"code.gitea.io/gitea/services/mailer"
 | 
				
			||||||
@@ -135,7 +136,7 @@ func SignIn(ctx *context.Context) {
 | 
				
			|||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	orderedOAuth2Names, oauth2Providers, err := models.GetActiveOAuth2Providers()
 | 
						orderedOAuth2Names, oauth2Providers, err := oauth2.GetActiveOAuth2Providers()
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		ctx.ServerError("UserSignIn", err)
 | 
							ctx.ServerError("UserSignIn", err)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
@@ -155,7 +156,7 @@ func SignIn(ctx *context.Context) {
 | 
				
			|||||||
func SignInPost(ctx *context.Context) {
 | 
					func SignInPost(ctx *context.Context) {
 | 
				
			||||||
	ctx.Data["Title"] = ctx.Tr("sign_in")
 | 
						ctx.Data["Title"] = ctx.Tr("sign_in")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	orderedOAuth2Names, oauth2Providers, err := models.GetActiveOAuth2Providers()
 | 
						orderedOAuth2Names, oauth2Providers, err := oauth2.GetActiveOAuth2Providers()
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		ctx.ServerError("UserSignIn", err)
 | 
							ctx.ServerError("UserSignIn", err)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
@@ -174,7 +175,7 @@ func SignInPost(ctx *context.Context) {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	form := web.GetForm(ctx).(*forms.SignInForm)
 | 
						form := web.GetForm(ctx).(*forms.SignInForm)
 | 
				
			||||||
	u, err := models.UserSignIn(form.UserName, form.Password)
 | 
						u, err := auth.UserSignIn(form.UserName, form.Password)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		if models.IsErrUserNotExist(err) {
 | 
							if models.IsErrUserNotExist(err) {
 | 
				
			||||||
			ctx.RenderWithErr(ctx.Tr("form.username_password_incorrect"), tplSignIn, &form)
 | 
								ctx.RenderWithErr(ctx.Tr("form.username_password_incorrect"), tplSignIn, &form)
 | 
				
			||||||
@@ -577,13 +578,13 @@ func SignInOAuth(ctx *context.Context) {
 | 
				
			|||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err = oauth2.Auth(loginSource.Name, ctx.Req, ctx.Resp); err != nil {
 | 
						if err = loginSource.Cfg.(*oauth2.Source).Callout(ctx.Req, ctx.Resp); err != nil {
 | 
				
			||||||
		if strings.Contains(err.Error(), "no provider for ") {
 | 
							if strings.Contains(err.Error(), "no provider for ") {
 | 
				
			||||||
			if err = models.ResetOAuth2(); err != nil {
 | 
								if err = oauth2.ResetOAuth2(); err != nil {
 | 
				
			||||||
				ctx.ServerError("SignIn", err)
 | 
									ctx.ServerError("SignIn", err)
 | 
				
			||||||
				return
 | 
									return
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			if err = oauth2.Auth(loginSource.Name, ctx.Req, ctx.Resp); err != nil {
 | 
								if err = loginSource.Cfg.(*oauth2.Source).Callout(ctx.Req, ctx.Resp); err != nil {
 | 
				
			||||||
				ctx.ServerError("SignIn", err)
 | 
									ctx.ServerError("SignIn", err)
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
@@ -631,7 +632,7 @@ func SignInOAuthCallback(ctx *context.Context) {
 | 
				
			|||||||
			}
 | 
								}
 | 
				
			||||||
			if len(missingFields) > 0 {
 | 
								if len(missingFields) > 0 {
 | 
				
			||||||
				log.Error("OAuth2 Provider %s returned empty or missing fields: %s", loginSource.Name, missingFields)
 | 
									log.Error("OAuth2 Provider %s returned empty or missing fields: %s", loginSource.Name, missingFields)
 | 
				
			||||||
				if loginSource.IsOAuth2() && loginSource.OAuth2().Provider == "openidConnect" {
 | 
									if loginSource.IsOAuth2() && loginSource.Cfg.(*oauth2.Source).Provider == "openidConnect" {
 | 
				
			||||||
					log.Error("You may need to change the 'OPENID_CONNECT_SCOPES' setting to request all required fields")
 | 
										log.Error("You may need to change the 'OPENID_CONNECT_SCOPES' setting to request all required fields")
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
				err = fmt.Errorf("OAuth2 Provider %s returned empty or missing fields: %s", loginSource.Name, missingFields)
 | 
									err = fmt.Errorf("OAuth2 Provider %s returned empty or missing fields: %s", loginSource.Name, missingFields)
 | 
				
			||||||
@@ -772,8 +773,7 @@ func handleOAuth2SignIn(ctx *context.Context, u *models.User, gothUser goth.User
 | 
				
			|||||||
// OAuth2UserLoginCallback attempts to handle the callback from the OAuth2 provider and if successful
 | 
					// OAuth2UserLoginCallback attempts to handle the callback from the OAuth2 provider and if successful
 | 
				
			||||||
// login the user
 | 
					// login the user
 | 
				
			||||||
func oAuth2UserLoginCallback(loginSource *models.LoginSource, request *http.Request, response http.ResponseWriter) (*models.User, goth.User, error) {
 | 
					func oAuth2UserLoginCallback(loginSource *models.LoginSource, request *http.Request, response http.ResponseWriter) (*models.User, goth.User, error) {
 | 
				
			||||||
	gothUser, err := oauth2.ProviderCallback(loginSource.Name, request, response)
 | 
						gothUser, err := loginSource.Cfg.(*oauth2.Source).Callback(request, response)
 | 
				
			||||||
 | 
					 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		if err.Error() == "securecookie: the value is too long" {
 | 
							if err.Error() == "securecookie: the value is too long" {
 | 
				
			||||||
			log.Error("OAuth2 Provider %s returned too long a token. Current max: %d. Either increase the [OAuth2] MAX_TOKEN_LENGTH or reduce the information returned from the OAuth2 provider", loginSource.Name, setting.OAuth2.MaxTokenLength)
 | 
								log.Error("OAuth2 Provider %s returned too long a token. Current max: %d. Either increase the [OAuth2] MAX_TOKEN_LENGTH or reduce the information returned from the OAuth2 provider", loginSource.Name, setting.OAuth2.MaxTokenLength)
 | 
				
			||||||
@@ -901,7 +901,7 @@ func LinkAccountPostSignIn(ctx *context.Context) {
 | 
				
			|||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	u, err := models.UserSignIn(signInForm.UserName, signInForm.Password)
 | 
						u, err := auth.UserSignIn(signInForm.UserName, signInForm.Password)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		if models.IsErrUserNotExist(err) {
 | 
							if models.IsErrUserNotExist(err) {
 | 
				
			||||||
			ctx.Data["user_exists"] = true
 | 
								ctx.Data["user_exists"] = true
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -20,6 +20,7 @@ import (
 | 
				
			|||||||
	"code.gitea.io/gitea/modules/util"
 | 
						"code.gitea.io/gitea/modules/util"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/web"
 | 
						"code.gitea.io/gitea/modules/web"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/web/middleware"
 | 
						"code.gitea.io/gitea/modules/web/middleware"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/services/auth"
 | 
				
			||||||
	"code.gitea.io/gitea/services/forms"
 | 
						"code.gitea.io/gitea/services/forms"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -290,7 +291,7 @@ func ConnectOpenIDPost(ctx *context.Context) {
 | 
				
			|||||||
	ctx.Data["EnableOpenIDSignUp"] = setting.Service.EnableOpenIDSignUp
 | 
						ctx.Data["EnableOpenIDSignUp"] = setting.Service.EnableOpenIDSignUp
 | 
				
			||||||
	ctx.Data["OpenID"] = oid
 | 
						ctx.Data["OpenID"] = oid
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	u, err := models.UserSignIn(form.UserName, form.Password)
 | 
						u, err := auth.UserSignIn(form.UserName, form.Password)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		if models.IsErrUserNotExist(err) {
 | 
							if models.IsErrUserNotExist(err) {
 | 
				
			||||||
			ctx.RenderWithErr(ctx.Tr("form.username_password_incorrect"), tplConnectOID, &form)
 | 
								ctx.RenderWithErr(ctx.Tr("form.username_password_incorrect"), tplConnectOID, &form)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -13,7 +13,6 @@ import (
 | 
				
			|||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"code.gitea.io/gitea/models"
 | 
						"code.gitea.io/gitea/models"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/auth/oauth2"
 | 
					 | 
				
			||||||
	"code.gitea.io/gitea/modules/base"
 | 
						"code.gitea.io/gitea/modules/base"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/context"
 | 
						"code.gitea.io/gitea/modules/context"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/log"
 | 
						"code.gitea.io/gitea/modules/log"
 | 
				
			||||||
@@ -21,6 +20,7 @@ import (
 | 
				
			|||||||
	"code.gitea.io/gitea/modules/timeutil"
 | 
						"code.gitea.io/gitea/modules/timeutil"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/web"
 | 
						"code.gitea.io/gitea/modules/web"
 | 
				
			||||||
	"code.gitea.io/gitea/services/auth"
 | 
						"code.gitea.io/gitea/services/auth"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/services/auth/source/oauth2"
 | 
				
			||||||
	"code.gitea.io/gitea/services/forms"
 | 
						"code.gitea.io/gitea/services/forms"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"gitea.com/go-chi/binding"
 | 
						"gitea.com/go-chi/binding"
 | 
				
			||||||
@@ -144,9 +144,9 @@ func newAccessTokenResponse(grant *models.OAuth2Grant, signingKey oauth2.JWTSign
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
	// generate access token to access the API
 | 
						// generate access token to access the API
 | 
				
			||||||
	expirationDate := timeutil.TimeStampNow().Add(setting.OAuth2.AccessTokenExpirationTime)
 | 
						expirationDate := timeutil.TimeStampNow().Add(setting.OAuth2.AccessTokenExpirationTime)
 | 
				
			||||||
	accessToken := &models.OAuth2Token{
 | 
						accessToken := &oauth2.Token{
 | 
				
			||||||
		GrantID: grant.ID,
 | 
							GrantID: grant.ID,
 | 
				
			||||||
		Type:    models.TypeAccessToken,
 | 
							Type:    oauth2.TypeAccessToken,
 | 
				
			||||||
		StandardClaims: jwt.StandardClaims{
 | 
							StandardClaims: jwt.StandardClaims{
 | 
				
			||||||
			ExpiresAt: expirationDate.AsTime().Unix(),
 | 
								ExpiresAt: expirationDate.AsTime().Unix(),
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
@@ -161,10 +161,10 @@ func newAccessTokenResponse(grant *models.OAuth2Grant, signingKey oauth2.JWTSign
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	// generate refresh token to request an access token after it expired later
 | 
						// generate refresh token to request an access token after it expired later
 | 
				
			||||||
	refreshExpirationDate := timeutil.TimeStampNow().Add(setting.OAuth2.RefreshTokenExpirationTime * 60 * 60).AsTime().Unix()
 | 
						refreshExpirationDate := timeutil.TimeStampNow().Add(setting.OAuth2.RefreshTokenExpirationTime * 60 * 60).AsTime().Unix()
 | 
				
			||||||
	refreshToken := &models.OAuth2Token{
 | 
						refreshToken := &oauth2.Token{
 | 
				
			||||||
		GrantID: grant.ID,
 | 
							GrantID: grant.ID,
 | 
				
			||||||
		Counter: grant.Counter,
 | 
							Counter: grant.Counter,
 | 
				
			||||||
		Type:    models.TypeRefreshToken,
 | 
							Type:    oauth2.TypeRefreshToken,
 | 
				
			||||||
		StandardClaims: jwt.StandardClaims{
 | 
							StandardClaims: jwt.StandardClaims{
 | 
				
			||||||
			ExpiresAt: refreshExpirationDate,
 | 
								ExpiresAt: refreshExpirationDate,
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
@@ -202,7 +202,7 @@ func newAccessTokenResponse(grant *models.OAuth2Grant, signingKey oauth2.JWTSign
 | 
				
			|||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		idToken := &models.OIDCToken{
 | 
							idToken := &oauth2.OIDCToken{
 | 
				
			||||||
			StandardClaims: jwt.StandardClaims{
 | 
								StandardClaims: jwt.StandardClaims{
 | 
				
			||||||
				ExpiresAt: expirationDate.AsTime().Unix(),
 | 
									ExpiresAt: expirationDate.AsTime().Unix(),
 | 
				
			||||||
				Issuer:    setting.AppURL,
 | 
									Issuer:    setting.AppURL,
 | 
				
			||||||
@@ -568,7 +568,7 @@ func AccessTokenOAuth(ctx *context.Context) {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func handleRefreshToken(ctx *context.Context, form forms.AccessTokenForm, signingKey oauth2.JWTSigningKey) {
 | 
					func handleRefreshToken(ctx *context.Context, form forms.AccessTokenForm, signingKey oauth2.JWTSigningKey) {
 | 
				
			||||||
	token, err := models.ParseOAuth2Token(form.RefreshToken)
 | 
						token, err := oauth2.ParseToken(form.RefreshToken)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		handleAccessTokenError(ctx, AccessTokenError{
 | 
							handleAccessTokenError(ctx, AccessTokenError{
 | 
				
			||||||
			ErrorCode:        AccessTokenErrorCodeUnauthorizedClient,
 | 
								ErrorCode:        AccessTokenErrorCodeUnauthorizedClient,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -18,6 +18,7 @@ import (
 | 
				
			|||||||
	"code.gitea.io/gitea/modules/setting"
 | 
						"code.gitea.io/gitea/modules/setting"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/timeutil"
 | 
						"code.gitea.io/gitea/modules/timeutil"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/web"
 | 
						"code.gitea.io/gitea/modules/web"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/services/auth"
 | 
				
			||||||
	"code.gitea.io/gitea/services/forms"
 | 
						"code.gitea.io/gitea/services/forms"
 | 
				
			||||||
	"code.gitea.io/gitea/services/mailer"
 | 
						"code.gitea.io/gitea/services/mailer"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
@@ -228,7 +229,7 @@ func DeleteAccount(ctx *context.Context) {
 | 
				
			|||||||
	ctx.Data["Title"] = ctx.Tr("settings")
 | 
						ctx.Data["Title"] = ctx.Tr("settings")
 | 
				
			||||||
	ctx.Data["PageIsSettingsAccount"] = true
 | 
						ctx.Data["PageIsSettingsAccount"] = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if _, err := models.UserSignIn(ctx.User.Name, ctx.Query("password")); err != nil {
 | 
						if _, err := auth.UserSignIn(ctx.User.Name, ctx.Query("password")); err != nil {
 | 
				
			||||||
		if models.IsErrUserNotExist(err) {
 | 
							if models.IsErrUserNotExist(err) {
 | 
				
			||||||
			loadAccountData(ctx)
 | 
								loadAccountData(ctx)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -12,6 +12,7 @@ import (
 | 
				
			|||||||
	"code.gitea.io/gitea/modules/base"
 | 
						"code.gitea.io/gitea/modules/base"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/context"
 | 
						"code.gitea.io/gitea/modules/context"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/setting"
 | 
						"code.gitea.io/gitea/modules/setting"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/services/auth/source/oauth2"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const (
 | 
					const (
 | 
				
			||||||
@@ -92,8 +93,8 @@ func loadSecurityData(ctx *context.Context) {
 | 
				
			|||||||
		if loginSource, err := models.GetLoginSourceByID(externalAccount.LoginSourceID); err == nil {
 | 
							if loginSource, err := models.GetLoginSourceByID(externalAccount.LoginSourceID); err == nil {
 | 
				
			||||||
			var providerDisplayName string
 | 
								var providerDisplayName string
 | 
				
			||||||
			if loginSource.IsOAuth2() {
 | 
								if loginSource.IsOAuth2() {
 | 
				
			||||||
				providerTechnicalName := loginSource.OAuth2().Provider
 | 
									providerTechnicalName := loginSource.Cfg.(*oauth2.Source).Provider
 | 
				
			||||||
				providerDisplayName = models.OAuth2Providers[providerTechnicalName].DisplayName
 | 
									providerDisplayName = oauth2.Providers[providerTechnicalName].DisplayName
 | 
				
			||||||
			} else {
 | 
								} else {
 | 
				
			||||||
				providerDisplayName = loginSource.Name
 | 
									providerDisplayName = loginSource.Name
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -27,7 +27,7 @@ import (
 | 
				
			|||||||
//
 | 
					//
 | 
				
			||||||
// The Session plugin is expected to be executed second, in order to skip authentication
 | 
					// The Session plugin is expected to be executed second, in order to skip authentication
 | 
				
			||||||
// for users that have already signed in.
 | 
					// for users that have already signed in.
 | 
				
			||||||
var authMethods = []Auth{
 | 
					var authMethods = []Method{
 | 
				
			||||||
	&OAuth2{},
 | 
						&OAuth2{},
 | 
				
			||||||
	&Basic{},
 | 
						&Basic{},
 | 
				
			||||||
	&Session{},
 | 
						&Session{},
 | 
				
			||||||
@@ -40,12 +40,12 @@ var (
 | 
				
			|||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Methods returns the instances of all registered methods
 | 
					// Methods returns the instances of all registered methods
 | 
				
			||||||
func Methods() []Auth {
 | 
					func Methods() []Method {
 | 
				
			||||||
	return authMethods
 | 
						return authMethods
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Register adds the specified instance to the list of available methods
 | 
					// Register adds the specified instance to the list of available methods
 | 
				
			||||||
func Register(method Auth) {
 | 
					func Register(method Method) {
 | 
				
			||||||
	authMethods = append(authMethods, method)
 | 
						authMethods = append(authMethods, method)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -57,7 +57,12 @@ func Init() {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
	specialInit()
 | 
						specialInit()
 | 
				
			||||||
	for _, method := range Methods() {
 | 
						for _, method := range Methods() {
 | 
				
			||||||
		err := method.Init()
 | 
							initializable, ok := method.(Initializable)
 | 
				
			||||||
 | 
							if !ok {
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							err := initializable.Init()
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			log.Error("Could not initialize '%s' auth method, error: %s", reflect.TypeOf(method).String(), err)
 | 
								log.Error("Could not initialize '%s' auth method, error: %s", reflect.TypeOf(method).String(), err)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@@ -68,7 +73,12 @@ func Init() {
 | 
				
			|||||||
// to release necessary resources
 | 
					// to release necessary resources
 | 
				
			||||||
func Free() {
 | 
					func Free() {
 | 
				
			||||||
	for _, method := range Methods() {
 | 
						for _, method := range Methods() {
 | 
				
			||||||
		err := method.Free()
 | 
							freeable, ok := method.(Freeable)
 | 
				
			||||||
 | 
							if !ok {
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							err := freeable.Free()
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			log.Error("Could not free '%s' auth method, error: %s", reflect.TypeOf(method).String(), err)
 | 
								log.Error("Could not free '%s' auth method, error: %s", reflect.TypeOf(method).String(), err)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -19,7 +19,8 @@ import (
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// Ensure the struct implements the interface.
 | 
					// Ensure the struct implements the interface.
 | 
				
			||||||
var (
 | 
					var (
 | 
				
			||||||
	_ Auth = &Basic{}
 | 
						_ Method = &Basic{}
 | 
				
			||||||
 | 
						_ Named  = &Basic{}
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Basic implements the Auth interface and authenticates requests (API requests
 | 
					// Basic implements the Auth interface and authenticates requests (API requests
 | 
				
			||||||
@@ -33,16 +34,6 @@ func (b *Basic) Name() string {
 | 
				
			|||||||
	return "basic"
 | 
						return "basic"
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Init does nothing as the Basic implementation does not need to allocate any resources
 | 
					 | 
				
			||||||
func (b *Basic) Init() error {
 | 
					 | 
				
			||||||
	return nil
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Free does nothing as the Basic implementation does not have to release any resources
 | 
					 | 
				
			||||||
func (b *Basic) Free() error {
 | 
					 | 
				
			||||||
	return nil
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Verify extracts and validates Basic data (username and password/token) from the
 | 
					// Verify extracts and validates Basic data (username and password/token) from the
 | 
				
			||||||
// "Authorization" header of the request and returns the corresponding user object for that
 | 
					// "Authorization" header of the request and returns the corresponding user object for that
 | 
				
			||||||
// name/token on successful validation.
 | 
					// name/token on successful validation.
 | 
				
			||||||
@@ -116,7 +107,7 @@ func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	log.Trace("Basic Authorization: Attempting SignIn for %s", uname)
 | 
						log.Trace("Basic Authorization: Attempting SignIn for %s", uname)
 | 
				
			||||||
	u, err := models.UserSignIn(uname, passwd)
 | 
						u, err := UserSignIn(uname, passwd)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		if !models.IsErrUserNotExist(err) {
 | 
							if !models.IsErrUserNotExist(err) {
 | 
				
			||||||
			log.Error("UserSignIn: %v", err)
 | 
								log.Error("UserSignIn: %v", err)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -12,30 +12,32 @@ import (
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// Ensure the struct implements the interface.
 | 
					// Ensure the struct implements the interface.
 | 
				
			||||||
var (
 | 
					var (
 | 
				
			||||||
	_ Auth = &Group{}
 | 
						_ Method        = &Group{}
 | 
				
			||||||
 | 
						_ Initializable = &Group{}
 | 
				
			||||||
 | 
						_ Freeable      = &Group{}
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Group implements the Auth interface with serval Auth.
 | 
					// Group implements the Auth interface with serval Auth.
 | 
				
			||||||
type Group struct {
 | 
					type Group struct {
 | 
				
			||||||
	methods []Auth
 | 
						methods []Method
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// NewGroup creates a new auth group
 | 
					// NewGroup creates a new auth group
 | 
				
			||||||
func NewGroup(methods ...Auth) *Group {
 | 
					func NewGroup(methods ...Method) *Group {
 | 
				
			||||||
	return &Group{
 | 
						return &Group{
 | 
				
			||||||
		methods: methods,
 | 
							methods: methods,
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Name represents the name of auth method
 | 
					 | 
				
			||||||
func (b *Group) Name() string {
 | 
					 | 
				
			||||||
	return "group"
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Init does nothing as the Basic implementation does not need to allocate any resources
 | 
					// Init does nothing as the Basic implementation does not need to allocate any resources
 | 
				
			||||||
func (b *Group) Init() error {
 | 
					func (b *Group) Init() error {
 | 
				
			||||||
	for _, m := range b.methods {
 | 
						for _, method := range b.methods {
 | 
				
			||||||
		if err := m.Init(); err != nil {
 | 
							initializable, ok := method.(Initializable)
 | 
				
			||||||
 | 
							if !ok {
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if err := initializable.Init(); err != nil {
 | 
				
			||||||
			return err
 | 
								return err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -44,8 +46,12 @@ func (b *Group) Init() error {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// Free does nothing as the Basic implementation does not have to release any resources
 | 
					// Free does nothing as the Basic implementation does not have to release any resources
 | 
				
			||||||
func (b *Group) Free() error {
 | 
					func (b *Group) Free() error {
 | 
				
			||||||
	for _, m := range b.methods {
 | 
						for _, method := range b.methods {
 | 
				
			||||||
		if err := m.Free(); err != nil {
 | 
							freeable, ok := method.(Freeable)
 | 
				
			||||||
 | 
							if !ok {
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if err := freeable.Free(); err != nil {
 | 
				
			||||||
			return err
 | 
								return err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -63,7 +69,9 @@ func (b *Group) Verify(req *http.Request, w http.ResponseWriter, store DataStore
 | 
				
			|||||||
		user := ssoMethod.Verify(req, w, store, sess)
 | 
							user := ssoMethod.Verify(req, w, store, sess)
 | 
				
			||||||
		if user != nil {
 | 
							if user != nil {
 | 
				
			||||||
			if store.GetData()["AuthedMethod"] == nil {
 | 
								if store.GetData()["AuthedMethod"] == nil {
 | 
				
			||||||
				store.GetData()["AuthedMethod"] = ssoMethod.Name()
 | 
									if named, ok := ssoMethod.(Named); ok {
 | 
				
			||||||
 | 
										store.GetData()["AuthedMethod"] = named.Name()
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			return user
 | 
								return user
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,6 +5,7 @@
 | 
				
			|||||||
package auth
 | 
					package auth
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"code.gitea.io/gitea/models"
 | 
						"code.gitea.io/gitea/models"
 | 
				
			||||||
@@ -18,18 +19,8 @@ type DataStore middleware.DataStore
 | 
				
			|||||||
// SessionStore represents a session store
 | 
					// SessionStore represents a session store
 | 
				
			||||||
type SessionStore session.Store
 | 
					type SessionStore session.Store
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Auth represents an authentication method (plugin) for HTTP requests.
 | 
					// Method represents an authentication method (plugin) for HTTP requests.
 | 
				
			||||||
type Auth interface {
 | 
					type Method interface {
 | 
				
			||||||
	Name() string
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Init should be called exactly once before using any of the other methods,
 | 
					 | 
				
			||||||
	// in order to allow the plugin to allocate necessary resources
 | 
					 | 
				
			||||||
	Init() error
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Free should be called exactly once before application closes, in order to
 | 
					 | 
				
			||||||
	// give chance to the plugin to free any allocated resources
 | 
					 | 
				
			||||||
	Free() error
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Verify tries to verify the authentication data contained in the request.
 | 
						// Verify tries to verify the authentication data contained in the request.
 | 
				
			||||||
	// If verification is successful returns either an existing user object (with id > 0)
 | 
						// If verification is successful returns either an existing user object (with id > 0)
 | 
				
			||||||
	// or a new user object (with id = 0) populated with the information that was found
 | 
						// or a new user object (with id = 0) populated with the information that was found
 | 
				
			||||||
@@ -37,3 +28,33 @@ type Auth interface {
 | 
				
			|||||||
	// Returns nil if verification fails.
 | 
						// Returns nil if verification fails.
 | 
				
			||||||
	Verify(http *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) *models.User
 | 
						Verify(http *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) *models.User
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Initializable represents a structure that requires initialization
 | 
				
			||||||
 | 
					// It usually should only be called once before anything else is called
 | 
				
			||||||
 | 
					type Initializable interface {
 | 
				
			||||||
 | 
						// Init should be called exactly once before using any of the other methods,
 | 
				
			||||||
 | 
						// in order to allow the plugin to allocate necessary resources
 | 
				
			||||||
 | 
						Init() error
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Named represents a named thing
 | 
				
			||||||
 | 
					type Named interface {
 | 
				
			||||||
 | 
						Name() string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Freeable represents a structure that is required to be freed
 | 
				
			||||||
 | 
					type Freeable interface {
 | 
				
			||||||
 | 
						// Free should be called exactly once before application closes, in order to
 | 
				
			||||||
 | 
						// give chance to the plugin to free any allocated resources
 | 
				
			||||||
 | 
						Free() error
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// PasswordAuthenticator represents a source of authentication
 | 
				
			||||||
 | 
					type PasswordAuthenticator interface {
 | 
				
			||||||
 | 
						Authenticate(user *models.User, login, password string) (*models.User, error)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// SynchronizableSource represents a source that can synchronize users
 | 
				
			||||||
 | 
					type SynchronizableSource interface {
 | 
				
			||||||
 | 
						Sync(ctx context.Context, updateExisting bool) error
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,11 +14,13 @@ import (
 | 
				
			|||||||
	"code.gitea.io/gitea/modules/log"
 | 
						"code.gitea.io/gitea/modules/log"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/timeutil"
 | 
						"code.gitea.io/gitea/modules/timeutil"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/web/middleware"
 | 
						"code.gitea.io/gitea/modules/web/middleware"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/services/auth/source/oauth2"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Ensure the struct implements the interface.
 | 
					// Ensure the struct implements the interface.
 | 
				
			||||||
var (
 | 
					var (
 | 
				
			||||||
	_ Auth = &OAuth2{}
 | 
						_ Method = &OAuth2{}
 | 
				
			||||||
 | 
						_ Named  = &OAuth2{}
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// CheckOAuthAccessToken returns uid of user from oauth token
 | 
					// CheckOAuthAccessToken returns uid of user from oauth token
 | 
				
			||||||
@@ -27,7 +29,7 @@ func CheckOAuthAccessToken(accessToken string) int64 {
 | 
				
			|||||||
	if !strings.Contains(accessToken, ".") {
 | 
						if !strings.Contains(accessToken, ".") {
 | 
				
			||||||
		return 0
 | 
							return 0
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	token, err := models.ParseOAuth2Token(accessToken)
 | 
						token, err := oauth2.ParseToken(accessToken)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		log.Trace("ParseOAuth2Token: %v", err)
 | 
							log.Trace("ParseOAuth2Token: %v", err)
 | 
				
			||||||
		return 0
 | 
							return 0
 | 
				
			||||||
@@ -36,7 +38,7 @@ func CheckOAuthAccessToken(accessToken string) int64 {
 | 
				
			|||||||
	if grant, err = models.GetOAuth2GrantByID(token.GrantID); err != nil || grant == nil {
 | 
						if grant, err = models.GetOAuth2GrantByID(token.GrantID); err != nil || grant == nil {
 | 
				
			||||||
		return 0
 | 
							return 0
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if token.Type != models.TypeAccessToken {
 | 
						if token.Type != oauth2.TypeAccessToken {
 | 
				
			||||||
		return 0
 | 
							return 0
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if token.ExpiresAt < time.Now().Unix() || token.IssuedAt > time.Now().Unix() {
 | 
						if token.ExpiresAt < time.Now().Unix() || token.IssuedAt > time.Now().Unix() {
 | 
				
			||||||
@@ -51,21 +53,11 @@ func CheckOAuthAccessToken(accessToken string) int64 {
 | 
				
			|||||||
type OAuth2 struct {
 | 
					type OAuth2 struct {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Init does nothing as the OAuth2 implementation does not need to allocate any resources
 | 
					 | 
				
			||||||
func (o *OAuth2) Init() error {
 | 
					 | 
				
			||||||
	return nil
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Name represents the name of auth method
 | 
					// Name represents the name of auth method
 | 
				
			||||||
func (o *OAuth2) Name() string {
 | 
					func (o *OAuth2) Name() string {
 | 
				
			||||||
	return "oauth2"
 | 
						return "oauth2"
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Free does nothing as the OAuth2 implementation does not have to release any resources
 | 
					 | 
				
			||||||
func (o *OAuth2) Free() error {
 | 
					 | 
				
			||||||
	return nil
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// userIDFromToken returns the user id corresponding to the OAuth token.
 | 
					// userIDFromToken returns the user id corresponding to the OAuth token.
 | 
				
			||||||
func (o *OAuth2) userIDFromToken(req *http.Request, store DataStore) int64 {
 | 
					func (o *OAuth2) userIDFromToken(req *http.Request, store DataStore) int64 {
 | 
				
			||||||
	_ = req.ParseForm()
 | 
						_ = req.ParseForm()
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -19,7 +19,8 @@ import (
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// Ensure the struct implements the interface.
 | 
					// Ensure the struct implements the interface.
 | 
				
			||||||
var (
 | 
					var (
 | 
				
			||||||
	_ Auth = &ReverseProxy{}
 | 
						_ Method = &ReverseProxy{}
 | 
				
			||||||
 | 
						_ Named  = &ReverseProxy{}
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// ReverseProxy implements the Auth interface, but actually relies on
 | 
					// ReverseProxy implements the Auth interface, but actually relies on
 | 
				
			||||||
@@ -44,16 +45,6 @@ func (r *ReverseProxy) Name() string {
 | 
				
			|||||||
	return "reverse_proxy"
 | 
						return "reverse_proxy"
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Init does nothing as the ReverseProxy implementation does not need initialization
 | 
					 | 
				
			||||||
func (r *ReverseProxy) Init() error {
 | 
					 | 
				
			||||||
	return nil
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Free does nothing as the ReverseProxy implementation does not have to release resources
 | 
					 | 
				
			||||||
func (r *ReverseProxy) Free() error {
 | 
					 | 
				
			||||||
	return nil
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Verify extracts the username from the "setting.ReverseProxyAuthUser" header
 | 
					// Verify extracts the username from the "setting.ReverseProxyAuthUser" header
 | 
				
			||||||
// of the request and returns the corresponding user object for that name.
 | 
					// of the request and returns the corresponding user object for that name.
 | 
				
			||||||
// Verification of header data is not performed as it should have already been done by
 | 
					// Verification of header data is not performed as it should have already been done by
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -13,7 +13,8 @@ import (
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// Ensure the struct implements the interface.
 | 
					// Ensure the struct implements the interface.
 | 
				
			||||||
var (
 | 
					var (
 | 
				
			||||||
	_ Auth = &Session{}
 | 
						_ Method = &Session{}
 | 
				
			||||||
 | 
						_ Named  = &Session{}
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Session checks if there is a user uid stored in the session and returns the user
 | 
					// Session checks if there is a user uid stored in the session and returns the user
 | 
				
			||||||
@@ -21,21 +22,11 @@ var (
 | 
				
			|||||||
type Session struct {
 | 
					type Session struct {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Init does nothing as the Session implementation does not need to allocate any resources
 | 
					 | 
				
			||||||
func (s *Session) Init() error {
 | 
					 | 
				
			||||||
	return nil
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Name represents the name of auth method
 | 
					// Name represents the name of auth method
 | 
				
			||||||
func (s *Session) Name() string {
 | 
					func (s *Session) Name() string {
 | 
				
			||||||
	return "session"
 | 
						return "session"
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Free does nothing as the Session implementation does not have to release any resources
 | 
					 | 
				
			||||||
func (s *Session) Free() error {
 | 
					 | 
				
			||||||
	return nil
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Verify checks if there is a user uid stored in the session and returns the user
 | 
					// Verify checks if there is a user uid stored in the session and returns the user
 | 
				
			||||||
// object for that uid.
 | 
					// object for that uid.
 | 
				
			||||||
// Returns nil if there is no user uid stored in the session.
 | 
					// Returns nil if there is no user uid stored in the session.
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										113
									
								
								services/auth/signin.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								services/auth/signin.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,113 @@
 | 
				
			|||||||
 | 
					// Copyright 2021 The Gitea Authors. All rights reserved.
 | 
				
			||||||
 | 
					// Use of this source code is governed by a MIT-style
 | 
				
			||||||
 | 
					// license that can be found in the LICENSE file.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package auth
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/models"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/log"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Register the sources
 | 
				
			||||||
 | 
						_ "code.gitea.io/gitea/services/auth/source/db"
 | 
				
			||||||
 | 
						_ "code.gitea.io/gitea/services/auth/source/ldap"
 | 
				
			||||||
 | 
						_ "code.gitea.io/gitea/services/auth/source/oauth2"
 | 
				
			||||||
 | 
						_ "code.gitea.io/gitea/services/auth/source/pam"
 | 
				
			||||||
 | 
						_ "code.gitea.io/gitea/services/auth/source/smtp"
 | 
				
			||||||
 | 
						_ "code.gitea.io/gitea/services/auth/source/sspi"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// UserSignIn validates user name and password.
 | 
				
			||||||
 | 
					func UserSignIn(username, password string) (*models.User, error) {
 | 
				
			||||||
 | 
						var user *models.User
 | 
				
			||||||
 | 
						if strings.Contains(username, "@") {
 | 
				
			||||||
 | 
							user = &models.User{Email: strings.ToLower(strings.TrimSpace(username))}
 | 
				
			||||||
 | 
							// check same email
 | 
				
			||||||
 | 
							cnt, err := models.Count(user)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return nil, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if cnt > 1 {
 | 
				
			||||||
 | 
								return nil, models.ErrEmailAlreadyUsed{
 | 
				
			||||||
 | 
									Email: user.Email,
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							trimmedUsername := strings.TrimSpace(username)
 | 
				
			||||||
 | 
							if len(trimmedUsername) == 0 {
 | 
				
			||||||
 | 
								return nil, models.ErrUserNotExist{Name: username}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							user = &models.User{LowerName: strings.ToLower(trimmedUsername)}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						hasUser, err := models.GetUser(user)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if hasUser {
 | 
				
			||||||
 | 
							source, err := models.GetLoginSourceByID(user.LoginSource)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return nil, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if !source.IsActive {
 | 
				
			||||||
 | 
								return nil, models.ErrLoginSourceNotActived
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							authenticator, ok := source.Cfg.(PasswordAuthenticator)
 | 
				
			||||||
 | 
							if !ok {
 | 
				
			||||||
 | 
								return nil, models.ErrUnsupportedLoginType
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							user, err := authenticator.Authenticate(user, username, password)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return nil, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// WARN: DON'T check user.IsActive, that will be checked on reqSign so that
 | 
				
			||||||
 | 
							// user could be hint to resend confirm email.
 | 
				
			||||||
 | 
							if user.ProhibitLogin {
 | 
				
			||||||
 | 
								return nil, models.ErrUserProhibitLogin{UID: user.ID, Name: user.Name}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return user, nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						sources, err := models.AllActiveLoginSources()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, source := range sources {
 | 
				
			||||||
 | 
							if !source.IsActive {
 | 
				
			||||||
 | 
								// don't try to authenticate non-active sources
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							authenticator, ok := source.Cfg.(PasswordAuthenticator)
 | 
				
			||||||
 | 
							if !ok {
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							authUser, err := authenticator.Authenticate(nil, username, password)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if err == nil {
 | 
				
			||||||
 | 
								if !authUser.ProhibitLogin {
 | 
				
			||||||
 | 
									return authUser, nil
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								err = models.ErrUserProhibitLogin{UID: authUser.ID, Name: authUser.Name}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if models.IsErrUserNotExist(err) {
 | 
				
			||||||
 | 
								log.Debug("Failed to login '%s' via '%s': %v", username, source.Name, err)
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								log.Warn("Failed to login '%s' via '%s': %v", username, source.Name, err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil, models.ErrUserNotExist{Name: username}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										21
									
								
								services/auth/source/db/assert_interface_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								services/auth/source/db/assert_interface_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,21 @@
 | 
				
			|||||||
 | 
					// Copyright 2021 The Gitea Authors. All rights reserved.
 | 
				
			||||||
 | 
					// Use of this source code is governed by a MIT-style
 | 
				
			||||||
 | 
					// license that can be found in the LICENSE file.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package db_test
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/models"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/services/auth"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/services/auth/source/db"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// This test file exists to assert that our Source exposes the interfaces that we expect
 | 
				
			||||||
 | 
					// It tightly binds the interfaces and implementation without breaking go import cycles
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type sourceInterface interface {
 | 
				
			||||||
 | 
						auth.PasswordAuthenticator
 | 
				
			||||||
 | 
						models.LoginConfig
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var _ (sourceInterface) = &db.Source{}
 | 
				
			||||||
							
								
								
									
										42
									
								
								services/auth/source/db/authenticate.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								services/auth/source/db/authenticate.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,42 @@
 | 
				
			|||||||
 | 
					// Copyright 2021 The Gitea Authors. All rights reserved.
 | 
				
			||||||
 | 
					// Use of this source code is governed by a MIT-style
 | 
				
			||||||
 | 
					// license that can be found in the LICENSE file.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package db
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/models"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/setting"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Authenticate authenticates the provided user against the DB
 | 
				
			||||||
 | 
					func Authenticate(user *models.User, login, password string) (*models.User, error) {
 | 
				
			||||||
 | 
						if user == nil {
 | 
				
			||||||
 | 
							return nil, models.ErrUserNotExist{Name: login}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if !user.IsPasswordSet() || !user.ValidatePassword(password) {
 | 
				
			||||||
 | 
							return nil, models.ErrUserNotExist{UID: user.ID, Name: user.Name}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Update password hash if server password hash algorithm have changed
 | 
				
			||||||
 | 
						if user.PasswdHashAlgo != setting.PasswordHashAlgo {
 | 
				
			||||||
 | 
							if err := user.SetPassword(password); err != nil {
 | 
				
			||||||
 | 
								return nil, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if err := models.UpdateUserCols(user, "passwd", "passwd_hash_algo", "salt"); err != nil {
 | 
				
			||||||
 | 
								return nil, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// WARN: DON'T check user.IsActive, that will be checked on reqSign so that
 | 
				
			||||||
 | 
						// user could be hint to resend confirm email.
 | 
				
			||||||
 | 
						if user.ProhibitLogin {
 | 
				
			||||||
 | 
							return nil, models.ErrUserProhibitLogin{
 | 
				
			||||||
 | 
								UID:  user.ID,
 | 
				
			||||||
 | 
								Name: user.Name,
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return user, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										31
									
								
								services/auth/source/db/source.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								services/auth/source/db/source.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,31 @@
 | 
				
			|||||||
 | 
					// Copyright 2021 The Gitea Authors. All rights reserved.
 | 
				
			||||||
 | 
					// Use of this source code is governed by a MIT-style
 | 
				
			||||||
 | 
					// license that can be found in the LICENSE file.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package db
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import "code.gitea.io/gitea/models"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Source is a password authentication service
 | 
				
			||||||
 | 
					type Source struct{}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// FromDB fills up an OAuth2Config from serialized format.
 | 
				
			||||||
 | 
					func (source *Source) FromDB(bs []byte) error {
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ToDB exports an SMTPConfig to a serialized format.
 | 
				
			||||||
 | 
					func (source *Source) ToDB() ([]byte, error) {
 | 
				
			||||||
 | 
						return nil, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Authenticate queries if login/password is valid against the PAM,
 | 
				
			||||||
 | 
					// and create a local user if success when enabled.
 | 
				
			||||||
 | 
					func (source *Source) Authenticate(user *models.User, login, password string) (*models.User, error) {
 | 
				
			||||||
 | 
						return Authenticate(user, login, password)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func init() {
 | 
				
			||||||
 | 
						models.RegisterLoginTypeConfig(models.LoginNoType, &Source{})
 | 
				
			||||||
 | 
						models.RegisterLoginTypeConfig(models.LoginPlain, &Source{})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,5 +1,4 @@
 | 
				
			|||||||
Gitea LDAP Authentication Module
 | 
					# Gitea LDAP Authentication Module
 | 
				
			||||||
===============================
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
## About
 | 
					## About
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										27
									
								
								services/auth/source/ldap/assert_interface_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								services/auth/source/ldap/assert_interface_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,27 @@
 | 
				
			|||||||
 | 
					// Copyright 2021 The Gitea Authors. All rights reserved.
 | 
				
			||||||
 | 
					// Use of this source code is governed by a MIT-style
 | 
				
			||||||
 | 
					// license that can be found in the LICENSE file.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package ldap_test
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/models"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/services/auth"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/services/auth/source/ldap"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// This test file exists to assert that our Source exposes the interfaces that we expect
 | 
				
			||||||
 | 
					// It tightly binds the interfaces and implementation without breaking go import cycles
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type sourceInterface interface {
 | 
				
			||||||
 | 
						auth.PasswordAuthenticator
 | 
				
			||||||
 | 
						auth.SynchronizableSource
 | 
				
			||||||
 | 
						models.SSHKeyProvider
 | 
				
			||||||
 | 
						models.LoginConfig
 | 
				
			||||||
 | 
						models.SkipVerifiable
 | 
				
			||||||
 | 
						models.HasTLSer
 | 
				
			||||||
 | 
						models.UseTLSer
 | 
				
			||||||
 | 
						models.LoginSourceSettable
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var _ (sourceInterface) = &ldap.Source{}
 | 
				
			||||||
							
								
								
									
										27
									
								
								services/auth/source/ldap/security_protocol.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								services/auth/source/ldap/security_protocol.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,27 @@
 | 
				
			|||||||
 | 
					// Copyright 2021 The Gitea Authors. All rights reserved.
 | 
				
			||||||
 | 
					// Use of this source code is governed by a MIT-style
 | 
				
			||||||
 | 
					// license that can be found in the LICENSE file.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package ldap
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// SecurityProtocol protocol type
 | 
				
			||||||
 | 
					type SecurityProtocol int
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Note: new type must be added at the end of list to maintain compatibility.
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						SecurityProtocolUnencrypted SecurityProtocol = iota
 | 
				
			||||||
 | 
						SecurityProtocolLDAPS
 | 
				
			||||||
 | 
						SecurityProtocolStartTLS
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// String returns the name of the SecurityProtocol
 | 
				
			||||||
 | 
					func (s SecurityProtocol) String() string {
 | 
				
			||||||
 | 
						return SecurityProtocolNames[s]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// SecurityProtocolNames contains the name of SecurityProtocol values.
 | 
				
			||||||
 | 
					var SecurityProtocolNames = map[SecurityProtocol]string{
 | 
				
			||||||
 | 
						SecurityProtocolUnencrypted: "Unencrypted",
 | 
				
			||||||
 | 
						SecurityProtocolLDAPS:       "LDAPS",
 | 
				
			||||||
 | 
						SecurityProtocolStartTLS:    "StartTLS",
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										120
									
								
								services/auth/source/ldap/source.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								services/auth/source/ldap/source.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,120 @@
 | 
				
			|||||||
 | 
					// Copyright 2021 The Gitea Authors. All rights reserved.
 | 
				
			||||||
 | 
					// Use of this source code is governed by a MIT-style
 | 
				
			||||||
 | 
					// license that can be found in the LICENSE file.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package ldap
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/models"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/secret"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/setting"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						jsoniter "github.com/json-iterator/go"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// .____     ________      _____ __________
 | 
				
			||||||
 | 
					// |    |    \______ \    /  _  \\______   \
 | 
				
			||||||
 | 
					// |    |     |    |  \  /  /_\  \|     ___/
 | 
				
			||||||
 | 
					// |    |___  |    `   \/    |    \    |
 | 
				
			||||||
 | 
					// |_______ \/_______  /\____|__  /____|
 | 
				
			||||||
 | 
					//         \/        \/         \/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Package ldap provide functions & structure to query a LDAP ldap directory
 | 
				
			||||||
 | 
					// For now, it's mainly tested again an MS Active Directory service, see README.md for more information
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Source Basic LDAP authentication service
 | 
				
			||||||
 | 
					type Source struct {
 | 
				
			||||||
 | 
						Name                  string // canonical name (ie. corporate.ad)
 | 
				
			||||||
 | 
						Host                  string // LDAP host
 | 
				
			||||||
 | 
						Port                  int    // port number
 | 
				
			||||||
 | 
						SecurityProtocol      SecurityProtocol
 | 
				
			||||||
 | 
						SkipVerify            bool
 | 
				
			||||||
 | 
						BindDN                string // DN to bind with
 | 
				
			||||||
 | 
						BindPasswordEncrypt   string // Encrypted Bind BN password
 | 
				
			||||||
 | 
						BindPassword          string // Bind DN password
 | 
				
			||||||
 | 
						UserBase              string // Base search path for users
 | 
				
			||||||
 | 
						UserDN                string // Template for the DN of the user for simple auth
 | 
				
			||||||
 | 
						AttributeUsername     string // Username attribute
 | 
				
			||||||
 | 
						AttributeName         string // First name attribute
 | 
				
			||||||
 | 
						AttributeSurname      string // Surname attribute
 | 
				
			||||||
 | 
						AttributeMail         string // E-mail attribute
 | 
				
			||||||
 | 
						AttributesInBind      bool   // fetch attributes in bind context (not user)
 | 
				
			||||||
 | 
						AttributeSSHPublicKey string // LDAP SSH Public Key attribute
 | 
				
			||||||
 | 
						SearchPageSize        uint32 // Search with paging page size
 | 
				
			||||||
 | 
						Filter                string // Query filter to validate entry
 | 
				
			||||||
 | 
						AdminFilter           string // Query filter to check if user is admin
 | 
				
			||||||
 | 
						RestrictedFilter      string // Query filter to check if user is restricted
 | 
				
			||||||
 | 
						Enabled               bool   // if this source is disabled
 | 
				
			||||||
 | 
						AllowDeactivateAll    bool   // Allow an empty search response to deactivate all users from this source
 | 
				
			||||||
 | 
						GroupsEnabled         bool   // if the group checking is enabled
 | 
				
			||||||
 | 
						GroupDN               string // Group Search Base
 | 
				
			||||||
 | 
						GroupFilter           string // Group Name Filter
 | 
				
			||||||
 | 
						GroupMemberUID        string // Group Attribute containing array of UserUID
 | 
				
			||||||
 | 
						UserUID               string // User Attribute listed in Group
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// reference to the loginSource
 | 
				
			||||||
 | 
						loginSource *models.LoginSource
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// FromDB fills up a LDAPConfig from serialized format.
 | 
				
			||||||
 | 
					func (source *Source) FromDB(bs []byte) error {
 | 
				
			||||||
 | 
						err := models.JSONUnmarshalHandleDoubleEncode(bs, &source)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if source.BindPasswordEncrypt != "" {
 | 
				
			||||||
 | 
							source.BindPassword, err = secret.DecryptSecret(setting.SecretKey, source.BindPasswordEncrypt)
 | 
				
			||||||
 | 
							source.BindPasswordEncrypt = ""
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ToDB exports a LDAPConfig to a serialized format.
 | 
				
			||||||
 | 
					func (source *Source) ToDB() ([]byte, error) {
 | 
				
			||||||
 | 
						var err error
 | 
				
			||||||
 | 
						source.BindPasswordEncrypt, err = secret.EncryptSecret(setting.SecretKey, source.BindPassword)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						source.BindPassword = ""
 | 
				
			||||||
 | 
						json := jsoniter.ConfigCompatibleWithStandardLibrary
 | 
				
			||||||
 | 
						return json.Marshal(source)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// SecurityProtocolName returns the name of configured security
 | 
				
			||||||
 | 
					// protocol.
 | 
				
			||||||
 | 
					func (source *Source) SecurityProtocolName() string {
 | 
				
			||||||
 | 
						return SecurityProtocolNames[source.SecurityProtocol]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// IsSkipVerify returns if SkipVerify is set
 | 
				
			||||||
 | 
					func (source *Source) IsSkipVerify() bool {
 | 
				
			||||||
 | 
						return source.SkipVerify
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// HasTLS returns if HasTLS
 | 
				
			||||||
 | 
					func (source *Source) HasTLS() bool {
 | 
				
			||||||
 | 
						return source.SecurityProtocol > SecurityProtocolUnencrypted
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// UseTLS returns if UseTLS
 | 
				
			||||||
 | 
					func (source *Source) UseTLS() bool {
 | 
				
			||||||
 | 
						return source.SecurityProtocol != SecurityProtocolUnencrypted
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ProvidesSSHKeys returns if this source provides SSH Keys
 | 
				
			||||||
 | 
					func (source *Source) ProvidesSSHKeys() bool {
 | 
				
			||||||
 | 
						return len(strings.TrimSpace(source.AttributeSSHPublicKey)) > 0
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// SetLoginSource sets the related LoginSource
 | 
				
			||||||
 | 
					func (source *Source) SetLoginSource(loginSource *models.LoginSource) {
 | 
				
			||||||
 | 
						source.loginSource = loginSource
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func init() {
 | 
				
			||||||
 | 
						models.RegisterLoginTypeConfig(models.LoginLDAP, &Source{})
 | 
				
			||||||
 | 
						models.RegisterLoginTypeConfig(models.LoginDLDAP, &Source{})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										93
									
								
								services/auth/source/ldap/source_authenticate.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								services/auth/source/ldap/source_authenticate.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,93 @@
 | 
				
			|||||||
 | 
					// Copyright 2021 The Gitea Authors. All rights reserved.
 | 
				
			||||||
 | 
					// Use of this source code is governed by a MIT-style
 | 
				
			||||||
 | 
					// license that can be found in the LICENSE file.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package ldap
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/models"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Authenticate queries if login/password is valid against the LDAP directory pool,
 | 
				
			||||||
 | 
					// and create a local user if success when enabled.
 | 
				
			||||||
 | 
					func (source *Source) Authenticate(user *models.User, login, password string) (*models.User, error) {
 | 
				
			||||||
 | 
						sr := source.SearchEntry(login, password, source.loginSource.Type == models.LoginDLDAP)
 | 
				
			||||||
 | 
						if sr == nil {
 | 
				
			||||||
 | 
							// User not in LDAP, do nothing
 | 
				
			||||||
 | 
							return nil, models.ErrUserNotExist{Name: login}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						isAttributeSSHPublicKeySet := len(strings.TrimSpace(source.AttributeSSHPublicKey)) > 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Update User admin flag if exist
 | 
				
			||||||
 | 
						if isExist, err := models.IsUserExist(0, sr.Username); err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						} else if isExist {
 | 
				
			||||||
 | 
							if user == nil {
 | 
				
			||||||
 | 
								user, err = models.GetUserByName(sr.Username)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									return nil, err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if user != nil && !user.ProhibitLogin {
 | 
				
			||||||
 | 
								cols := make([]string, 0)
 | 
				
			||||||
 | 
								if len(source.AdminFilter) > 0 && user.IsAdmin != sr.IsAdmin {
 | 
				
			||||||
 | 
									// Change existing admin flag only if AdminFilter option is set
 | 
				
			||||||
 | 
									user.IsAdmin = sr.IsAdmin
 | 
				
			||||||
 | 
									cols = append(cols, "is_admin")
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if !user.IsAdmin && len(source.RestrictedFilter) > 0 && user.IsRestricted != sr.IsRestricted {
 | 
				
			||||||
 | 
									// Change existing restricted flag only if RestrictedFilter option is set
 | 
				
			||||||
 | 
									user.IsRestricted = sr.IsRestricted
 | 
				
			||||||
 | 
									cols = append(cols, "is_restricted")
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if len(cols) > 0 {
 | 
				
			||||||
 | 
									err = models.UpdateUserCols(user, cols...)
 | 
				
			||||||
 | 
									if err != nil {
 | 
				
			||||||
 | 
										return nil, err
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if user != nil {
 | 
				
			||||||
 | 
							if isAttributeSSHPublicKeySet && models.SynchronizePublicKeys(user, source.loginSource, sr.SSHPublicKey) {
 | 
				
			||||||
 | 
								return user, models.RewriteAllPublicKeys()
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return user, nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Fallback.
 | 
				
			||||||
 | 
						if len(sr.Username) == 0 {
 | 
				
			||||||
 | 
							sr.Username = login
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if len(sr.Mail) == 0 {
 | 
				
			||||||
 | 
							sr.Mail = fmt.Sprintf("%s@localhost", sr.Username)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						user = &models.User{
 | 
				
			||||||
 | 
							LowerName:    strings.ToLower(sr.Username),
 | 
				
			||||||
 | 
							Name:         sr.Username,
 | 
				
			||||||
 | 
							FullName:     composeFullName(sr.Name, sr.Surname, sr.Username),
 | 
				
			||||||
 | 
							Email:        sr.Mail,
 | 
				
			||||||
 | 
							LoginType:    source.loginSource.Type,
 | 
				
			||||||
 | 
							LoginSource:  source.loginSource.ID,
 | 
				
			||||||
 | 
							LoginName:    login,
 | 
				
			||||||
 | 
							IsActive:     true,
 | 
				
			||||||
 | 
							IsAdmin:      sr.IsAdmin,
 | 
				
			||||||
 | 
							IsRestricted: sr.IsRestricted,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						err := models.CreateUser(user)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err == nil && isAttributeSSHPublicKeySet && models.AddPublicKeysBySource(user, source.loginSource, sr.SSHPublicKey) {
 | 
				
			||||||
 | 
							err = models.RewriteAllPublicKeys()
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return user, err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -3,8 +3,6 @@
 | 
				
			|||||||
// Use of this source code is governed by a MIT-style
 | 
					// Use of this source code is governed by a MIT-style
 | 
				
			||||||
// license that can be found in the LICENSE file.
 | 
					// license that can be found in the LICENSE file.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Package ldap provide functions & structure to query a LDAP ldap directory
 | 
					 | 
				
			||||||
// For now, it's mainly tested again an MS Active Directory service, see README.md for more information
 | 
					 | 
				
			||||||
package ldap
 | 
					package ldap
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
@@ -17,47 +15,6 @@ import (
 | 
				
			|||||||
	"github.com/go-ldap/ldap/v3"
 | 
						"github.com/go-ldap/ldap/v3"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// SecurityProtocol protocol type
 | 
					 | 
				
			||||||
type SecurityProtocol int
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Note: new type must be added at the end of list to maintain compatibility.
 | 
					 | 
				
			||||||
const (
 | 
					 | 
				
			||||||
	SecurityProtocolUnencrypted SecurityProtocol = iota
 | 
					 | 
				
			||||||
	SecurityProtocolLDAPS
 | 
					 | 
				
			||||||
	SecurityProtocolStartTLS
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Source Basic LDAP authentication service
 | 
					 | 
				
			||||||
type Source struct {
 | 
					 | 
				
			||||||
	Name                  string // canonical name (ie. corporate.ad)
 | 
					 | 
				
			||||||
	Host                  string // LDAP host
 | 
					 | 
				
			||||||
	Port                  int    // port number
 | 
					 | 
				
			||||||
	SecurityProtocol      SecurityProtocol
 | 
					 | 
				
			||||||
	SkipVerify            bool
 | 
					 | 
				
			||||||
	BindDN                string // DN to bind with
 | 
					 | 
				
			||||||
	BindPasswordEncrypt   string // Encrypted Bind BN password
 | 
					 | 
				
			||||||
	BindPassword          string // Bind DN password
 | 
					 | 
				
			||||||
	UserBase              string // Base search path for users
 | 
					 | 
				
			||||||
	UserDN                string // Template for the DN of the user for simple auth
 | 
					 | 
				
			||||||
	AttributeUsername     string // Username attribute
 | 
					 | 
				
			||||||
	AttributeName         string // First name attribute
 | 
					 | 
				
			||||||
	AttributeSurname      string // Surname attribute
 | 
					 | 
				
			||||||
	AttributeMail         string // E-mail attribute
 | 
					 | 
				
			||||||
	AttributesInBind      bool   // fetch attributes in bind context (not user)
 | 
					 | 
				
			||||||
	AttributeSSHPublicKey string // LDAP SSH Public Key attribute
 | 
					 | 
				
			||||||
	SearchPageSize        uint32 // Search with paging page size
 | 
					 | 
				
			||||||
	Filter                string // Query filter to validate entry
 | 
					 | 
				
			||||||
	AdminFilter           string // Query filter to check if user is admin
 | 
					 | 
				
			||||||
	RestrictedFilter      string // Query filter to check if user is restricted
 | 
					 | 
				
			||||||
	Enabled               bool   // if this source is disabled
 | 
					 | 
				
			||||||
	AllowDeactivateAll    bool   // Allow an empty search response to deactivate all users from this source
 | 
					 | 
				
			||||||
	GroupsEnabled         bool   // if the group checking is enabled
 | 
					 | 
				
			||||||
	GroupDN               string // Group Search Base
 | 
					 | 
				
			||||||
	GroupFilter           string // Group Name Filter
 | 
					 | 
				
			||||||
	GroupMemberUID        string // Group Attribute containing array of UserUID
 | 
					 | 
				
			||||||
	UserUID               string // User Attribute listed in Group
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// SearchResult : user data
 | 
					// SearchResult : user data
 | 
				
			||||||
type SearchResult struct {
 | 
					type SearchResult struct {
 | 
				
			||||||
	Username     string   // Username
 | 
						Username     string   // Username
 | 
				
			||||||
							
								
								
									
										184
									
								
								services/auth/source/ldap/source_sync.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										184
									
								
								services/auth/source/ldap/source_sync.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,184 @@
 | 
				
			|||||||
 | 
					// Copyright 2021 The Gitea Authors. All rights reserved.
 | 
				
			||||||
 | 
					// Use of this source code is governed by a MIT-style
 | 
				
			||||||
 | 
					// license that can be found in the LICENSE file.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package ldap
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/models"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/log"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Sync causes this ldap source to synchronize its users with the db
 | 
				
			||||||
 | 
					func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
 | 
				
			||||||
 | 
						log.Trace("Doing: SyncExternalUsers[%s]", source.loginSource.Name)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var existingUsers []int64
 | 
				
			||||||
 | 
						isAttributeSSHPublicKeySet := len(strings.TrimSpace(source.AttributeSSHPublicKey)) > 0
 | 
				
			||||||
 | 
						var sshKeysNeedUpdate bool
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Find all users with this login type - FIXME: Should this be an iterator?
 | 
				
			||||||
 | 
						users, err := models.GetUsersBySource(source.loginSource)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							log.Error("SyncExternalUsers: %v", err)
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						select {
 | 
				
			||||||
 | 
						case <-ctx.Done():
 | 
				
			||||||
 | 
							log.Warn("SyncExternalUsers: Cancelled before update of %s", source.loginSource.Name)
 | 
				
			||||||
 | 
							return models.ErrCancelledf("Before update of %s", source.loginSource.Name)
 | 
				
			||||||
 | 
						default:
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						sr, err := source.SearchEntries()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							log.Error("SyncExternalUsers LDAP source failure [%s], skipped", source.loginSource.Name)
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if len(sr) == 0 {
 | 
				
			||||||
 | 
							if !source.AllowDeactivateAll {
 | 
				
			||||||
 | 
								log.Error("LDAP search found no entries but did not report an error. Refusing to deactivate all users")
 | 
				
			||||||
 | 
								return nil
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							log.Warn("LDAP search found no entries but did not report an error. All users will be deactivated as per settings")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, su := range sr {
 | 
				
			||||||
 | 
							select {
 | 
				
			||||||
 | 
							case <-ctx.Done():
 | 
				
			||||||
 | 
								log.Warn("SyncExternalUsers: Cancelled at update of %s before completed update of users", source.loginSource.Name)
 | 
				
			||||||
 | 
								// Rewrite authorized_keys file if LDAP Public SSH Key attribute is set and any key was added or removed
 | 
				
			||||||
 | 
								if sshKeysNeedUpdate {
 | 
				
			||||||
 | 
									err = models.RewriteAllPublicKeys()
 | 
				
			||||||
 | 
									if err != nil {
 | 
				
			||||||
 | 
										log.Error("RewriteAllPublicKeys: %v", err)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								return models.ErrCancelledf("During update of %s before completed update of users", source.loginSource.Name)
 | 
				
			||||||
 | 
							default:
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if len(su.Username) == 0 {
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if len(su.Mail) == 0 {
 | 
				
			||||||
 | 
								su.Mail = fmt.Sprintf("%s@localhost", su.Username)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							var usr *models.User
 | 
				
			||||||
 | 
							// Search for existing user
 | 
				
			||||||
 | 
							for _, du := range users {
 | 
				
			||||||
 | 
								if du.LowerName == strings.ToLower(su.Username) {
 | 
				
			||||||
 | 
									usr = du
 | 
				
			||||||
 | 
									break
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							fullName := composeFullName(su.Name, su.Surname, su.Username)
 | 
				
			||||||
 | 
							// If no existing user found, create one
 | 
				
			||||||
 | 
							if usr == nil {
 | 
				
			||||||
 | 
								log.Trace("SyncExternalUsers[%s]: Creating user %s", source.loginSource.Name, su.Username)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								usr = &models.User{
 | 
				
			||||||
 | 
									LowerName:    strings.ToLower(su.Username),
 | 
				
			||||||
 | 
									Name:         su.Username,
 | 
				
			||||||
 | 
									FullName:     fullName,
 | 
				
			||||||
 | 
									LoginType:    source.loginSource.Type,
 | 
				
			||||||
 | 
									LoginSource:  source.loginSource.ID,
 | 
				
			||||||
 | 
									LoginName:    su.Username,
 | 
				
			||||||
 | 
									Email:        su.Mail,
 | 
				
			||||||
 | 
									IsAdmin:      su.IsAdmin,
 | 
				
			||||||
 | 
									IsRestricted: su.IsRestricted,
 | 
				
			||||||
 | 
									IsActive:     true,
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								err = models.CreateUser(usr)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									log.Error("SyncExternalUsers[%s]: Error creating user %s: %v", source.loginSource.Name, su.Username, err)
 | 
				
			||||||
 | 
								} else if isAttributeSSHPublicKeySet {
 | 
				
			||||||
 | 
									log.Trace("SyncExternalUsers[%s]: Adding LDAP Public SSH Keys for user %s", source.loginSource.Name, usr.Name)
 | 
				
			||||||
 | 
									if models.AddPublicKeysBySource(usr, source.loginSource, su.SSHPublicKey) {
 | 
				
			||||||
 | 
										sshKeysNeedUpdate = true
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							} else if updateExisting {
 | 
				
			||||||
 | 
								existingUsers = append(existingUsers, usr.ID)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// Synchronize SSH Public Key if that attribute is set
 | 
				
			||||||
 | 
								if isAttributeSSHPublicKeySet && models.SynchronizePublicKeys(usr, source.loginSource, su.SSHPublicKey) {
 | 
				
			||||||
 | 
									sshKeysNeedUpdate = true
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// Check if user data has changed
 | 
				
			||||||
 | 
								if (len(source.AdminFilter) > 0 && usr.IsAdmin != su.IsAdmin) ||
 | 
				
			||||||
 | 
									(len(source.RestrictedFilter) > 0 && usr.IsRestricted != su.IsRestricted) ||
 | 
				
			||||||
 | 
									!strings.EqualFold(usr.Email, su.Mail) ||
 | 
				
			||||||
 | 
									usr.FullName != fullName ||
 | 
				
			||||||
 | 
									!usr.IsActive {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									log.Trace("SyncExternalUsers[%s]: Updating user %s", source.loginSource.Name, usr.Name)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									usr.FullName = fullName
 | 
				
			||||||
 | 
									usr.Email = su.Mail
 | 
				
			||||||
 | 
									// Change existing admin flag only if AdminFilter option is set
 | 
				
			||||||
 | 
									if len(source.AdminFilter) > 0 {
 | 
				
			||||||
 | 
										usr.IsAdmin = su.IsAdmin
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									// Change existing restricted flag only if RestrictedFilter option is set
 | 
				
			||||||
 | 
									if !usr.IsAdmin && len(source.RestrictedFilter) > 0 {
 | 
				
			||||||
 | 
										usr.IsRestricted = su.IsRestricted
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									usr.IsActive = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									err = models.UpdateUserCols(usr, "full_name", "email", "is_admin", "is_restricted", "is_active")
 | 
				
			||||||
 | 
									if err != nil {
 | 
				
			||||||
 | 
										log.Error("SyncExternalUsers[%s]: Error updating user %s: %v", source.loginSource.Name, usr.Name, err)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Rewrite authorized_keys file if LDAP Public SSH Key attribute is set and any key was added or removed
 | 
				
			||||||
 | 
						if sshKeysNeedUpdate {
 | 
				
			||||||
 | 
							err = models.RewriteAllPublicKeys()
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								log.Error("RewriteAllPublicKeys: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						select {
 | 
				
			||||||
 | 
						case <-ctx.Done():
 | 
				
			||||||
 | 
							log.Warn("SyncExternalUsers: Cancelled during update of %s before delete users", source.loginSource.Name)
 | 
				
			||||||
 | 
							return models.ErrCancelledf("During update of %s before delete users", source.loginSource.Name)
 | 
				
			||||||
 | 
						default:
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Deactivate users not present in LDAP
 | 
				
			||||||
 | 
						if updateExisting {
 | 
				
			||||||
 | 
							for _, usr := range users {
 | 
				
			||||||
 | 
								found := false
 | 
				
			||||||
 | 
								for _, uid := range existingUsers {
 | 
				
			||||||
 | 
									if usr.ID == uid {
 | 
				
			||||||
 | 
										found = true
 | 
				
			||||||
 | 
										break
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if !found {
 | 
				
			||||||
 | 
									log.Trace("SyncExternalUsers[%s]: Deactivating user %s", source.loginSource.Name, usr.Name)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									usr.IsActive = false
 | 
				
			||||||
 | 
									err = models.UpdateUserCols(usr, "is_active")
 | 
				
			||||||
 | 
									if err != nil {
 | 
				
			||||||
 | 
										log.Error("SyncExternalUsers[%s]: Error deactivating user %s: %v", source.loginSource.Name, usr.Name, err)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										19
									
								
								services/auth/source/ldap/util.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								services/auth/source/ldap/util.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
				
			|||||||
 | 
					// Copyright 2021 The Gitea Authors. All rights reserved.
 | 
				
			||||||
 | 
					// Use of this source code is governed by a MIT-style
 | 
				
			||||||
 | 
					// license that can be found in the LICENSE file.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package ldap
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// composeFullName composes a firstname surname or username
 | 
				
			||||||
 | 
					func composeFullName(firstname, surname, username string) string {
 | 
				
			||||||
 | 
						switch {
 | 
				
			||||||
 | 
						case len(firstname) == 0 && len(surname) == 0:
 | 
				
			||||||
 | 
							return username
 | 
				
			||||||
 | 
						case len(firstname) == 0:
 | 
				
			||||||
 | 
							return surname
 | 
				
			||||||
 | 
						case len(surname) == 0:
 | 
				
			||||||
 | 
							return firstname
 | 
				
			||||||
 | 
						default:
 | 
				
			||||||
 | 
							return firstname + " " + surname
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										23
									
								
								services/auth/source/oauth2/assert_interface_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								services/auth/source/oauth2/assert_interface_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
				
			|||||||
 | 
					// Copyright 2021 The Gitea Authors. All rights reserved.
 | 
				
			||||||
 | 
					// Use of this source code is governed by a MIT-style
 | 
				
			||||||
 | 
					// license that can be found in the LICENSE file.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package oauth2_test
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/models"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/services/auth"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/services/auth/source/oauth2"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// This test file exists to assert that our Source exposes the interfaces that we expect
 | 
				
			||||||
 | 
					// It tightly binds the interfaces and implementation without breaking go import cycles
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type sourceInterface interface {
 | 
				
			||||||
 | 
						models.LoginConfig
 | 
				
			||||||
 | 
						models.LoginSourceSettable
 | 
				
			||||||
 | 
						models.RegisterableSource
 | 
				
			||||||
 | 
						auth.PasswordAuthenticator
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var _ (sourceInterface) = &oauth2.Source{}
 | 
				
			||||||
							
								
								
									
										83
									
								
								services/auth/source/oauth2/init.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								services/auth/source/oauth2/init.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,83 @@
 | 
				
			|||||||
 | 
					// Copyright 2021 The Gitea Authors. All rights reserved.
 | 
				
			||||||
 | 
					// Use of this source code is governed by a MIT-style
 | 
				
			||||||
 | 
					// license that can be found in the LICENSE file.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package oauth2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/models"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/log"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/setting"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/google/uuid"
 | 
				
			||||||
 | 
						"github.com/markbates/goth/gothic"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// SessionTableName is the table name that OAuth2 will use to store things
 | 
				
			||||||
 | 
					const SessionTableName = "oauth2_session"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// UsersStoreKey is the key for the store
 | 
				
			||||||
 | 
					const UsersStoreKey = "gitea-oauth2-sessions"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ProviderHeaderKey is the HTTP header key
 | 
				
			||||||
 | 
					const ProviderHeaderKey = "gitea-oauth2-provider"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Init initializes the oauth source
 | 
				
			||||||
 | 
					func Init() error {
 | 
				
			||||||
 | 
						if err := InitSigningKey(); err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						store, err := models.CreateStore(SessionTableName, UsersStoreKey)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// according to the Goth lib:
 | 
				
			||||||
 | 
						// set the maxLength of the cookies stored on the disk to a larger number to prevent issues with:
 | 
				
			||||||
 | 
						// securecookie: the value is too long
 | 
				
			||||||
 | 
						// when using OpenID Connect , since this can contain a large amount of extra information in the id_token
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Note, when using the FilesystemStore only the session.ID is written to a browser cookie, so this is explicit for the storage on disk
 | 
				
			||||||
 | 
						store.MaxLength(setting.OAuth2.MaxTokenLength)
 | 
				
			||||||
 | 
						gothic.Store = store
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						gothic.SetState = func(req *http.Request) string {
 | 
				
			||||||
 | 
							return uuid.New().String()
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						gothic.GetProviderName = func(req *http.Request) (string, error) {
 | 
				
			||||||
 | 
							return req.Header.Get(ProviderHeaderKey), nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return initOAuth2LoginSources()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ResetOAuth2 clears existing OAuth2 providers and loads them from DB
 | 
				
			||||||
 | 
					func ResetOAuth2() error {
 | 
				
			||||||
 | 
						ClearProviders()
 | 
				
			||||||
 | 
						return initOAuth2LoginSources()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// initOAuth2LoginSources is used to load and register all active OAuth2 providers
 | 
				
			||||||
 | 
					func initOAuth2LoginSources() error {
 | 
				
			||||||
 | 
						loginSources, _ := models.GetActiveOAuth2ProviderLoginSources()
 | 
				
			||||||
 | 
						for _, source := range loginSources {
 | 
				
			||||||
 | 
							oauth2Source, ok := source.Cfg.(*Source)
 | 
				
			||||||
 | 
							if !ok {
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							err := oauth2Source.RegisterSource()
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								log.Critical("Unable to register source: %s due to Error: %v. This source will be disabled.", source.Name, err)
 | 
				
			||||||
 | 
								source.IsActive = false
 | 
				
			||||||
 | 
								if err = models.UpdateSource(source); err != nil {
 | 
				
			||||||
 | 
									log.Critical("Unable to update source %s to disable it. Error: %v", err)
 | 
				
			||||||
 | 
									return err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,20 +1,18 @@
 | 
				
			|||||||
// Copyright 2017 The Gitea Authors. All rights reserved.
 | 
					// Copyright 2021 The Gitea Authors. All rights reserved.
 | 
				
			||||||
// Use of this source code is governed by a MIT-style
 | 
					// Use of this source code is governed by a MIT-style
 | 
				
			||||||
// license that can be found in the LICENSE file.
 | 
					// license that can be found in the LICENSE file.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
package oauth2
 | 
					package oauth2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"net/http"
 | 
					 | 
				
			||||||
	"net/url"
 | 
						"net/url"
 | 
				
			||||||
 | 
						"sort"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/models"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/log"
 | 
						"code.gitea.io/gitea/modules/log"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/setting"
 | 
						"code.gitea.io/gitea/modules/setting"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	uuid "github.com/google/uuid"
 | 
					 | 
				
			||||||
	"github.com/lafriks/xormstore"
 | 
					 | 
				
			||||||
	"github.com/markbates/goth"
 | 
						"github.com/markbates/goth"
 | 
				
			||||||
	"github.com/markbates/goth/gothic"
 | 
					 | 
				
			||||||
	"github.com/markbates/goth/providers/bitbucket"
 | 
						"github.com/markbates/goth/providers/bitbucket"
 | 
				
			||||||
	"github.com/markbates/goth/providers/discord"
 | 
						"github.com/markbates/goth/providers/discord"
 | 
				
			||||||
	"github.com/markbates/goth/providers/dropbox"
 | 
						"github.com/markbates/goth/providers/dropbox"
 | 
				
			||||||
@@ -28,79 +26,94 @@ import (
 | 
				
			|||||||
	"github.com/markbates/goth/providers/openidConnect"
 | 
						"github.com/markbates/goth/providers/openidConnect"
 | 
				
			||||||
	"github.com/markbates/goth/providers/twitter"
 | 
						"github.com/markbates/goth/providers/twitter"
 | 
				
			||||||
	"github.com/markbates/goth/providers/yandex"
 | 
						"github.com/markbates/goth/providers/yandex"
 | 
				
			||||||
	"xorm.io/xorm"
 | 
					 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
var (
 | 
					// Provider describes the display values of a single OAuth2 provider
 | 
				
			||||||
	sessionUsersStoreKey = "gitea-oauth2-sessions"
 | 
					type Provider struct {
 | 
				
			||||||
	providerHeaderKey    = "gitea-oauth2-provider"
 | 
						Name             string
 | 
				
			||||||
)
 | 
						DisplayName      string
 | 
				
			||||||
 | 
						Image            string
 | 
				
			||||||
// CustomURLMapping describes the urls values to use when customizing OAuth2 provider URLs
 | 
						CustomURLMapping *CustomURLMapping
 | 
				
			||||||
type CustomURLMapping struct {
 | 
					 | 
				
			||||||
	AuthURL    string
 | 
					 | 
				
			||||||
	TokenURL   string
 | 
					 | 
				
			||||||
	ProfileURL string
 | 
					 | 
				
			||||||
	EmailURL   string
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Init initialize the setup of the OAuth2 library
 | 
					// Providers contains the map of registered OAuth2 providers in Gitea (based on goth)
 | 
				
			||||||
func Init(x *xorm.Engine) error {
 | 
					// key is used to map the OAuth2Provider with the goth provider type (also in LoginSource.OAuth2Config.Provider)
 | 
				
			||||||
	store, err := xormstore.NewOptions(x, xormstore.Options{
 | 
					// value is used to store display data
 | 
				
			||||||
		TableName: "oauth2_session",
 | 
					var Providers = map[string]Provider{
 | 
				
			||||||
	}, []byte(sessionUsersStoreKey))
 | 
						"bitbucket": {Name: "bitbucket", DisplayName: "Bitbucket", Image: "/assets/img/auth/bitbucket.png"},
 | 
				
			||||||
 | 
						"dropbox":   {Name: "dropbox", DisplayName: "Dropbox", Image: "/assets/img/auth/dropbox.png"},
 | 
				
			||||||
 | 
						"facebook":  {Name: "facebook", DisplayName: "Facebook", Image: "/assets/img/auth/facebook.png"},
 | 
				
			||||||
 | 
						"github": {
 | 
				
			||||||
 | 
							Name: "github", DisplayName: "GitHub", Image: "/assets/img/auth/github.png",
 | 
				
			||||||
 | 
							CustomURLMapping: &CustomURLMapping{
 | 
				
			||||||
 | 
								TokenURL:   github.TokenURL,
 | 
				
			||||||
 | 
								AuthURL:    github.AuthURL,
 | 
				
			||||||
 | 
								ProfileURL: github.ProfileURL,
 | 
				
			||||||
 | 
								EmailURL:   github.EmailURL,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						"gitlab": {
 | 
				
			||||||
 | 
							Name: "gitlab", DisplayName: "GitLab", Image: "/assets/img/auth/gitlab.png",
 | 
				
			||||||
 | 
							CustomURLMapping: &CustomURLMapping{
 | 
				
			||||||
 | 
								TokenURL:   gitlab.TokenURL,
 | 
				
			||||||
 | 
								AuthURL:    gitlab.AuthURL,
 | 
				
			||||||
 | 
								ProfileURL: gitlab.ProfileURL,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						"gplus":         {Name: "gplus", DisplayName: "Google", Image: "/assets/img/auth/google.png"},
 | 
				
			||||||
 | 
						"openidConnect": {Name: "openidConnect", DisplayName: "OpenID Connect", Image: "/assets/img/auth/openid_connect.svg"},
 | 
				
			||||||
 | 
						"twitter":       {Name: "twitter", DisplayName: "Twitter", Image: "/assets/img/auth/twitter.png"},
 | 
				
			||||||
 | 
						"discord":       {Name: "discord", DisplayName: "Discord", Image: "/assets/img/auth/discord.png"},
 | 
				
			||||||
 | 
						"gitea": {
 | 
				
			||||||
 | 
							Name: "gitea", DisplayName: "Gitea", Image: "/assets/img/auth/gitea.png",
 | 
				
			||||||
 | 
							CustomURLMapping: &CustomURLMapping{
 | 
				
			||||||
 | 
								TokenURL:   gitea.TokenURL,
 | 
				
			||||||
 | 
								AuthURL:    gitea.AuthURL,
 | 
				
			||||||
 | 
								ProfileURL: gitea.ProfileURL,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						"nextcloud": {
 | 
				
			||||||
 | 
							Name: "nextcloud", DisplayName: "Nextcloud", Image: "/assets/img/auth/nextcloud.png",
 | 
				
			||||||
 | 
							CustomURLMapping: &CustomURLMapping{
 | 
				
			||||||
 | 
								TokenURL:   nextcloud.TokenURL,
 | 
				
			||||||
 | 
								AuthURL:    nextcloud.AuthURL,
 | 
				
			||||||
 | 
								ProfileURL: nextcloud.ProfileURL,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						"yandex": {Name: "yandex", DisplayName: "Yandex", Image: "/assets/img/auth/yandex.png"},
 | 
				
			||||||
 | 
						"mastodon": {
 | 
				
			||||||
 | 
							Name: "mastodon", DisplayName: "Mastodon", Image: "/assets/img/auth/mastodon.png",
 | 
				
			||||||
 | 
							CustomURLMapping: &CustomURLMapping{
 | 
				
			||||||
 | 
								AuthURL: mastodon.InstanceURL,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetActiveOAuth2Providers returns the map of configured active OAuth2 providers
 | 
				
			||||||
 | 
					// key is used as technical name (like in the callbackURL)
 | 
				
			||||||
 | 
					// values to display
 | 
				
			||||||
 | 
					func GetActiveOAuth2Providers() ([]string, map[string]Provider, error) {
 | 
				
			||||||
 | 
						// Maybe also separate used and unused providers so we can force the registration of only 1 active provider for each type
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						loginSources, err := models.GetActiveOAuth2ProviderLoginSources()
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return err
 | 
							return nil, nil, err
 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	// according to the Goth lib:
 | 
					 | 
				
			||||||
	// set the maxLength of the cookies stored on the disk to a larger number to prevent issues with:
 | 
					 | 
				
			||||||
	// securecookie: the value is too long
 | 
					 | 
				
			||||||
	// when using OpenID Connect , since this can contain a large amount of extra information in the id_token
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Note, when using the FilesystemStore only the session.ID is written to a browser cookie, so this is explicit for the storage on disk
 | 
					 | 
				
			||||||
	store.MaxLength(setting.OAuth2.MaxTokenLength)
 | 
					 | 
				
			||||||
	gothic.Store = store
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	gothic.SetState = func(req *http.Request) string {
 | 
					 | 
				
			||||||
		return uuid.New().String()
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	gothic.GetProviderName = func(req *http.Request) (string, error) {
 | 
						var orderedKeys []string
 | 
				
			||||||
		return req.Header.Get(providerHeaderKey), nil
 | 
						providers := make(map[string]Provider)
 | 
				
			||||||
 | 
						for _, source := range loginSources {
 | 
				
			||||||
 | 
							prov := Providers[source.Cfg.(*Source).Provider]
 | 
				
			||||||
 | 
							if source.Cfg.(*Source).IconURL != "" {
 | 
				
			||||||
 | 
								prov.Image = source.Cfg.(*Source).IconURL
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							providers[source.Name] = prov
 | 
				
			||||||
 | 
							orderedKeys = append(orderedKeys, source.Name)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return nil
 | 
						sort.Strings(orderedKeys)
 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Auth OAuth2 auth service
 | 
						return orderedKeys, providers, nil
 | 
				
			||||||
func Auth(provider string, request *http.Request, response http.ResponseWriter) error {
 | 
					 | 
				
			||||||
	// not sure if goth is thread safe (?) when using multiple providers
 | 
					 | 
				
			||||||
	request.Header.Set(providerHeaderKey, provider)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// 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
 | 
					 | 
				
			||||||
	//gothic.BeginAuthHandler(response, request)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	url, err := gothic.GetAuthURL(response, request)
 | 
					 | 
				
			||||||
	if err == nil {
 | 
					 | 
				
			||||||
		http.Redirect(response, request, url, http.StatusTemporaryRedirect)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return err
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// ProviderCallback handles OAuth callback, resolve to a goth user and send back to original url
 | 
					 | 
				
			||||||
// this will trigger a new authentication request, but because we save it in the session we can use that
 | 
					 | 
				
			||||||
func ProviderCallback(provider string, request *http.Request, response http.ResponseWriter) (goth.User, error) {
 | 
					 | 
				
			||||||
	// not sure if goth is thread safe (?) when using multiple providers
 | 
					 | 
				
			||||||
	request.Header.Set(providerHeaderKey, provider)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	user, err := gothic.CompleteUserAuth(response, request)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		return user, err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return user, nil
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// RegisterProvider register a OAuth2 provider in goth lib
 | 
					// RegisterProvider register a OAuth2 provider in goth lib
 | 
				
			||||||
@@ -242,58 +255,3 @@ func createProvider(providerName, providerType, clientID, clientSecret, openIDCo
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	return provider, err
 | 
						return provider, err
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
// GetDefaultTokenURL return the default token url for the given provider
 | 
					 | 
				
			||||||
func GetDefaultTokenURL(provider string) string {
 | 
					 | 
				
			||||||
	switch provider {
 | 
					 | 
				
			||||||
	case "github":
 | 
					 | 
				
			||||||
		return github.TokenURL
 | 
					 | 
				
			||||||
	case "gitlab":
 | 
					 | 
				
			||||||
		return gitlab.TokenURL
 | 
					 | 
				
			||||||
	case "gitea":
 | 
					 | 
				
			||||||
		return gitea.TokenURL
 | 
					 | 
				
			||||||
	case "nextcloud":
 | 
					 | 
				
			||||||
		return nextcloud.TokenURL
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return ""
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// GetDefaultAuthURL return the default authorize url for the given provider
 | 
					 | 
				
			||||||
func GetDefaultAuthURL(provider string) string {
 | 
					 | 
				
			||||||
	switch provider {
 | 
					 | 
				
			||||||
	case "github":
 | 
					 | 
				
			||||||
		return github.AuthURL
 | 
					 | 
				
			||||||
	case "gitlab":
 | 
					 | 
				
			||||||
		return gitlab.AuthURL
 | 
					 | 
				
			||||||
	case "gitea":
 | 
					 | 
				
			||||||
		return gitea.AuthURL
 | 
					 | 
				
			||||||
	case "nextcloud":
 | 
					 | 
				
			||||||
		return nextcloud.AuthURL
 | 
					 | 
				
			||||||
	case "mastodon":
 | 
					 | 
				
			||||||
		return mastodon.InstanceURL
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return ""
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// GetDefaultProfileURL return the default profile url for the given provider
 | 
					 | 
				
			||||||
func GetDefaultProfileURL(provider string) string {
 | 
					 | 
				
			||||||
	switch provider {
 | 
					 | 
				
			||||||
	case "github":
 | 
					 | 
				
			||||||
		return github.ProfileURL
 | 
					 | 
				
			||||||
	case "gitlab":
 | 
					 | 
				
			||||||
		return gitlab.ProfileURL
 | 
					 | 
				
			||||||
	case "gitea":
 | 
					 | 
				
			||||||
		return gitea.ProfileURL
 | 
					 | 
				
			||||||
	case "nextcloud":
 | 
					 | 
				
			||||||
		return nextcloud.ProfileURL
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return ""
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// GetDefaultEmailURL return the default email url for the given provider
 | 
					 | 
				
			||||||
func GetDefaultEmailURL(provider string) string {
 | 
					 | 
				
			||||||
	if provider == "github" {
 | 
					 | 
				
			||||||
		return github.EmailURL
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return ""
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										51
									
								
								services/auth/source/oauth2/source.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								services/auth/source/oauth2/source.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,51 @@
 | 
				
			|||||||
 | 
					// Copyright 2021 The Gitea Authors. All rights reserved.
 | 
				
			||||||
 | 
					// Use of this source code is governed by a MIT-style
 | 
				
			||||||
 | 
					// license that can be found in the LICENSE file.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package oauth2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/models"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						jsoniter "github.com/json-iterator/go"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ________      _____          __  .__     ________
 | 
				
			||||||
 | 
					// \_____  \    /  _  \  __ ___/  |_|  |__  \_____  \
 | 
				
			||||||
 | 
					// /   |   \  /  /_\  \|  |  \   __\  |  \  /  ____/
 | 
				
			||||||
 | 
					// /    |    \/    |    \  |  /|  | |   Y  \/       \
 | 
				
			||||||
 | 
					// \_______  /\____|__  /____/ |__| |___|  /\_______ \
 | 
				
			||||||
 | 
					//         \/         \/                 \/         \/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Source holds configuration for the OAuth2 login source.
 | 
				
			||||||
 | 
					type Source struct {
 | 
				
			||||||
 | 
						Provider                      string
 | 
				
			||||||
 | 
						ClientID                      string
 | 
				
			||||||
 | 
						ClientSecret                  string
 | 
				
			||||||
 | 
						OpenIDConnectAutoDiscoveryURL string
 | 
				
			||||||
 | 
						CustomURLMapping              *CustomURLMapping
 | 
				
			||||||
 | 
						IconURL                       string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// reference to the loginSource
 | 
				
			||||||
 | 
						loginSource *models.LoginSource
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// FromDB fills up an OAuth2Config from serialized format.
 | 
				
			||||||
 | 
					func (source *Source) FromDB(bs []byte) error {
 | 
				
			||||||
 | 
						return models.JSONUnmarshalHandleDoubleEncode(bs, &source)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ToDB exports an SMTPConfig to a serialized format.
 | 
				
			||||||
 | 
					func (source *Source) ToDB() ([]byte, error) {
 | 
				
			||||||
 | 
						json := jsoniter.ConfigCompatibleWithStandardLibrary
 | 
				
			||||||
 | 
						return json.Marshal(source)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// SetLoginSource sets the related LoginSource
 | 
				
			||||||
 | 
					func (source *Source) SetLoginSource(loginSource *models.LoginSource) {
 | 
				
			||||||
 | 
						source.loginSource = loginSource
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func init() {
 | 
				
			||||||
 | 
						models.RegisterLoginTypeConfig(models.LoginOAuth2, &Source{})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										15
									
								
								services/auth/source/oauth2/source_authenticate.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								services/auth/source/oauth2/source_authenticate.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,15 @@
 | 
				
			|||||||
 | 
					// Copyright 2021 The Gitea Authors. All rights reserved.
 | 
				
			||||||
 | 
					// Use of this source code is governed by a MIT-style
 | 
				
			||||||
 | 
					// license that can be found in the LICENSE file.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package oauth2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/models"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/services/auth/source/db"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Authenticate falls back to the db authenticator
 | 
				
			||||||
 | 
					func (source *Source) Authenticate(user *models.User, login, password string) (*models.User, error) {
 | 
				
			||||||
 | 
						return db.Authenticate(user, login, password)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										42
									
								
								services/auth/source/oauth2/source_callout.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								services/auth/source/oauth2/source_callout.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,42 @@
 | 
				
			|||||||
 | 
					// Copyright 2021 The Gitea Authors. All rights reserved.
 | 
				
			||||||
 | 
					// Use of this source code is governed by a MIT-style
 | 
				
			||||||
 | 
					// license that can be found in the LICENSE file.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package oauth2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/markbates/goth"
 | 
				
			||||||
 | 
						"github.com/markbates/goth/gothic"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Callout redirects request/response pair to authenticate against the provider
 | 
				
			||||||
 | 
					func (source *Source) Callout(request *http.Request, response http.ResponseWriter) error {
 | 
				
			||||||
 | 
						// not sure if goth is thread safe (?) when using multiple providers
 | 
				
			||||||
 | 
						request.Header.Set(ProviderHeaderKey, source.loginSource.Name)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// don't use the default gothic begin handler to prevent issues when some error occurs
 | 
				
			||||||
 | 
						// normally the gothic library will write some custom stuff to the response instead of our own nice error page
 | 
				
			||||||
 | 
						//gothic.BeginAuthHandler(response, request)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						url, err := gothic.GetAuthURL(response, request)
 | 
				
			||||||
 | 
						if err == nil {
 | 
				
			||||||
 | 
							http.Redirect(response, request, url, http.StatusTemporaryRedirect)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Callback handles OAuth callback, resolve to a goth user and send back to original url
 | 
				
			||||||
 | 
					// this will trigger a new authentication request, but because we save it in the session we can use that
 | 
				
			||||||
 | 
					func (source *Source) Callback(request *http.Request, response http.ResponseWriter) (goth.User, error) {
 | 
				
			||||||
 | 
						// not sure if goth is thread safe (?) when using multiple providers
 | 
				
			||||||
 | 
						request.Header.Set(ProviderHeaderKey, source.loginSource.Name)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						user, err := gothic.CompleteUserAuth(response, request)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return user, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return user, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										30
									
								
								services/auth/source/oauth2/source_register.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								services/auth/source/oauth2/source_register.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,30 @@
 | 
				
			|||||||
 | 
					// Copyright 2021 The Gitea Authors. All rights reserved.
 | 
				
			||||||
 | 
					// Use of this source code is governed by a MIT-style
 | 
				
			||||||
 | 
					// license that can be found in the LICENSE file.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package oauth2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/models"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// RegisterSource causes an OAuth2 configuration to be registered
 | 
				
			||||||
 | 
					func (source *Source) RegisterSource() error {
 | 
				
			||||||
 | 
						err := RegisterProvider(source.loginSource.Name, source.Provider, source.ClientID, source.ClientSecret, source.OpenIDConnectAutoDiscoveryURL, source.CustomURLMapping)
 | 
				
			||||||
 | 
						return wrapOpenIDConnectInitializeError(err, source.loginSource.Name, source)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// UnregisterSource causes an OAuth2 configuration to be unregistered
 | 
				
			||||||
 | 
					func (source *Source) UnregisterSource() error {
 | 
				
			||||||
 | 
						RemoveProvider(source.loginSource.Name)
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// wrapOpenIDConnectInitializeError is used to wrap the error but this cannot be done in modules/auth/oauth2
 | 
				
			||||||
 | 
					// inside oauth2: import cycle not allowed models -> modules/auth/oauth2 -> models
 | 
				
			||||||
 | 
					func wrapOpenIDConnectInitializeError(err error, providerName string, source *Source) error {
 | 
				
			||||||
 | 
						if err != nil && source.Provider == "openidConnect" {
 | 
				
			||||||
 | 
							err = models.ErrOpenIDConnectInitialize{ProviderName: providerName, OpenIDConnectAutoDiscoveryURL: source.OpenIDConnectAutoDiscoveryURL, Cause: err}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										94
									
								
								services/auth/source/oauth2/token.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								services/auth/source/oauth2/token.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,94 @@
 | 
				
			|||||||
 | 
					// Copyright 2021 The Gitea Authors. All rights reserved.
 | 
				
			||||||
 | 
					// Use of this source code is governed by a MIT-style
 | 
				
			||||||
 | 
					// license that can be found in the LICENSE file.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package oauth2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/timeutil"
 | 
				
			||||||
 | 
						"github.com/dgrijalva/jwt-go"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ___________     __
 | 
				
			||||||
 | 
					// \__    ___/___ |  | __ ____   ____
 | 
				
			||||||
 | 
					//   |    | /  _ \|  |/ // __ \ /    \
 | 
				
			||||||
 | 
					//   |    |(  <_> )    <\  ___/|   |  \
 | 
				
			||||||
 | 
					//   |____| \____/|__|_ \\___  >___|  /
 | 
				
			||||||
 | 
					//                     \/    \/     \/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Token represents an Oauth grant
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// TokenType represents the type of token for an oauth application
 | 
				
			||||||
 | 
					type TokenType int
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						// TypeAccessToken is a token with short lifetime to access the api
 | 
				
			||||||
 | 
						TypeAccessToken TokenType = 0
 | 
				
			||||||
 | 
						// TypeRefreshToken is token with long lifetime to refresh access tokens obtained by the client
 | 
				
			||||||
 | 
						TypeRefreshToken = iota
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Token represents a JWT token used to authenticate a client
 | 
				
			||||||
 | 
					type Token struct {
 | 
				
			||||||
 | 
						GrantID int64     `json:"gnt"`
 | 
				
			||||||
 | 
						Type    TokenType `json:"tt"`
 | 
				
			||||||
 | 
						Counter int64     `json:"cnt,omitempty"`
 | 
				
			||||||
 | 
						jwt.StandardClaims
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ParseToken parses a signed jwt string
 | 
				
			||||||
 | 
					func ParseToken(jwtToken string) (*Token, error) {
 | 
				
			||||||
 | 
						parsedToken, err := jwt.ParseWithClaims(jwtToken, &Token{}, func(token *jwt.Token) (interface{}, error) {
 | 
				
			||||||
 | 
							if token.Method == nil || token.Method.Alg() != DefaultSigningKey.SigningMethod().Alg() {
 | 
				
			||||||
 | 
								return nil, fmt.Errorf("unexpected signing algo: %v", token.Header["alg"])
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return DefaultSigningKey.VerifyKey(), nil
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						var token *Token
 | 
				
			||||||
 | 
						var ok bool
 | 
				
			||||||
 | 
						if token, ok = parsedToken.Claims.(*Token); !ok || !parsedToken.Valid {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("invalid token")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return token, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// SignToken signs the token with the JWT secret
 | 
				
			||||||
 | 
					func (token *Token) SignToken() (string, error) {
 | 
				
			||||||
 | 
						token.IssuedAt = time.Now().Unix()
 | 
				
			||||||
 | 
						jwtToken := jwt.NewWithClaims(DefaultSigningKey.SigningMethod(), token)
 | 
				
			||||||
 | 
						DefaultSigningKey.PreProcessToken(jwtToken)
 | 
				
			||||||
 | 
						return jwtToken.SignedString(DefaultSigningKey.SignKey())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// OIDCToken represents an OpenID Connect id_token
 | 
				
			||||||
 | 
					type OIDCToken struct {
 | 
				
			||||||
 | 
						jwt.StandardClaims
 | 
				
			||||||
 | 
						Nonce string `json:"nonce,omitempty"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Scope profile
 | 
				
			||||||
 | 
						Name              string             `json:"name,omitempty"`
 | 
				
			||||||
 | 
						PreferredUsername string             `json:"preferred_username,omitempty"`
 | 
				
			||||||
 | 
						Profile           string             `json:"profile,omitempty"`
 | 
				
			||||||
 | 
						Picture           string             `json:"picture,omitempty"`
 | 
				
			||||||
 | 
						Website           string             `json:"website,omitempty"`
 | 
				
			||||||
 | 
						Locale            string             `json:"locale,omitempty"`
 | 
				
			||||||
 | 
						UpdatedAt         timeutil.TimeStamp `json:"updated_at,omitempty"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Scope email
 | 
				
			||||||
 | 
						Email         string `json:"email,omitempty"`
 | 
				
			||||||
 | 
						EmailVerified bool   `json:"email_verified,omitempty"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// SignToken signs an id_token with the (symmetric) client secret key
 | 
				
			||||||
 | 
					func (token *OIDCToken) SignToken(signingKey JWTSigningKey) (string, error) {
 | 
				
			||||||
 | 
						token.IssuedAt = time.Now().Unix()
 | 
				
			||||||
 | 
						jwtToken := jwt.NewWithClaims(signingKey.SigningMethod(), token)
 | 
				
			||||||
 | 
						signingKey.PreProcessToken(jwtToken)
 | 
				
			||||||
 | 
						return jwtToken.SignedString(signingKey.SignKey())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										24
									
								
								services/auth/source/oauth2/urlmapping.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								services/auth/source/oauth2/urlmapping.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,24 @@
 | 
				
			|||||||
 | 
					// Copyright 2021 The Gitea Authors. All rights reserved.
 | 
				
			||||||
 | 
					// Use of this source code is governed by a MIT-style
 | 
				
			||||||
 | 
					// license that can be found in the LICENSE file.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package oauth2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// CustomURLMapping describes the urls values to use when customizing OAuth2 provider URLs
 | 
				
			||||||
 | 
					type CustomURLMapping struct {
 | 
				
			||||||
 | 
						AuthURL    string
 | 
				
			||||||
 | 
						TokenURL   string
 | 
				
			||||||
 | 
						ProfileURL string
 | 
				
			||||||
 | 
						EmailURL   string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// DefaultCustomURLMappings contains the map of default URL's for OAuth2 providers that are allowed to have custom urls
 | 
				
			||||||
 | 
					// key is used to map the OAuth2Provider
 | 
				
			||||||
 | 
					// value is the mapping as defined for the OAuth2Provider
 | 
				
			||||||
 | 
					var DefaultCustomURLMappings = map[string]*CustomURLMapping{
 | 
				
			||||||
 | 
						"github":    Providers["github"].CustomURLMapping,
 | 
				
			||||||
 | 
						"gitlab":    Providers["gitlab"].CustomURLMapping,
 | 
				
			||||||
 | 
						"gitea":     Providers["gitea"].CustomURLMapping,
 | 
				
			||||||
 | 
						"nextcloud": Providers["nextcloud"].CustomURLMapping,
 | 
				
			||||||
 | 
						"mastodon":  Providers["mastodon"].CustomURLMapping,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										22
									
								
								services/auth/source/pam/assert_interface_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								services/auth/source/pam/assert_interface_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,22 @@
 | 
				
			|||||||
 | 
					// Copyright 2021 The Gitea Authors. All rights reserved.
 | 
				
			||||||
 | 
					// Use of this source code is governed by a MIT-style
 | 
				
			||||||
 | 
					// license that can be found in the LICENSE file.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package pam_test
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/models"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/services/auth"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/services/auth/source/pam"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// This test file exists to assert that our Source exposes the interfaces that we expect
 | 
				
			||||||
 | 
					// It tightly binds the interfaces and implementation without breaking go import cycles
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type sourceInterface interface {
 | 
				
			||||||
 | 
						auth.PasswordAuthenticator
 | 
				
			||||||
 | 
						models.LoginConfig
 | 
				
			||||||
 | 
						models.LoginSourceSettable
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var _ (sourceInterface) = &pam.Source{}
 | 
				
			||||||
							
								
								
									
										47
									
								
								services/auth/source/pam/source.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								services/auth/source/pam/source.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,47 @@
 | 
				
			|||||||
 | 
					// Copyright 2021 The Gitea Authors. All rights reserved.
 | 
				
			||||||
 | 
					// Use of this source code is governed by a MIT-style
 | 
				
			||||||
 | 
					// license that can be found in the LICENSE file.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package pam
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/models"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						jsoniter "github.com/json-iterator/go"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// __________  _____      _____
 | 
				
			||||||
 | 
					// \______   \/  _  \    /     \
 | 
				
			||||||
 | 
					//  |     ___/  /_\  \  /  \ /  \
 | 
				
			||||||
 | 
					//  |    |  /    |    \/    Y    \
 | 
				
			||||||
 | 
					//  |____|  \____|__  /\____|__  /
 | 
				
			||||||
 | 
					//                  \/         \/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Source holds configuration for the PAM login source.
 | 
				
			||||||
 | 
					type Source struct {
 | 
				
			||||||
 | 
						ServiceName string // pam service (e.g. system-auth)
 | 
				
			||||||
 | 
						EmailDomain string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// reference to the loginSource
 | 
				
			||||||
 | 
						loginSource *models.LoginSource
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// FromDB fills up a PAMConfig from serialized format.
 | 
				
			||||||
 | 
					func (source *Source) FromDB(bs []byte) error {
 | 
				
			||||||
 | 
						return models.JSONUnmarshalHandleDoubleEncode(bs, &source)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ToDB exports a PAMConfig to a serialized format.
 | 
				
			||||||
 | 
					func (source *Source) ToDB() ([]byte, error) {
 | 
				
			||||||
 | 
						json := jsoniter.ConfigCompatibleWithStandardLibrary
 | 
				
			||||||
 | 
						return json.Marshal(source)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// SetLoginSource sets the related LoginSource
 | 
				
			||||||
 | 
					func (source *Source) SetLoginSource(loginSource *models.LoginSource) {
 | 
				
			||||||
 | 
						source.loginSource = loginSource
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func init() {
 | 
				
			||||||
 | 
						models.RegisterLoginTypeConfig(models.LoginPAM, &Source{})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										62
									
								
								services/auth/source/pam/source_authenticate.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								services/auth/source/pam/source_authenticate.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,62 @@
 | 
				
			|||||||
 | 
					// Copyright 2021 The Gitea Authors. All rights reserved.
 | 
				
			||||||
 | 
					// Use of this source code is governed by a MIT-style
 | 
				
			||||||
 | 
					// license that can be found in the LICENSE file.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package pam
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/models"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/auth/pam"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/setting"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/google/uuid"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Authenticate queries if login/password is valid against the PAM,
 | 
				
			||||||
 | 
					// and create a local user if success when enabled.
 | 
				
			||||||
 | 
					func (source *Source) Authenticate(user *models.User, login, password string) (*models.User, error) {
 | 
				
			||||||
 | 
						pamLogin, err := pam.Auth(source.ServiceName, login, password)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							if strings.Contains(err.Error(), "Authentication failure") {
 | 
				
			||||||
 | 
								return nil, models.ErrUserNotExist{Name: login}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if user != nil {
 | 
				
			||||||
 | 
							return user, nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Allow PAM sources with `@` in their name, like from Active Directory
 | 
				
			||||||
 | 
						username := pamLogin
 | 
				
			||||||
 | 
						email := pamLogin
 | 
				
			||||||
 | 
						idx := strings.Index(pamLogin, "@")
 | 
				
			||||||
 | 
						if idx > -1 {
 | 
				
			||||||
 | 
							username = pamLogin[:idx]
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if models.ValidateEmail(email) != nil {
 | 
				
			||||||
 | 
							if source.EmailDomain != "" {
 | 
				
			||||||
 | 
								email = fmt.Sprintf("%s@%s", username, source.EmailDomain)
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								email = fmt.Sprintf("%s@%s", username, setting.Service.NoReplyAddress)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if models.ValidateEmail(email) != nil {
 | 
				
			||||||
 | 
								email = uuid.New().String() + "@localhost"
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						user = &models.User{
 | 
				
			||||||
 | 
							LowerName:   strings.ToLower(username),
 | 
				
			||||||
 | 
							Name:        username,
 | 
				
			||||||
 | 
							Email:       email,
 | 
				
			||||||
 | 
							Passwd:      password,
 | 
				
			||||||
 | 
							LoginType:   models.LoginPAM,
 | 
				
			||||||
 | 
							LoginSource: source.loginSource.ID,
 | 
				
			||||||
 | 
							LoginName:   login, // This is what the user typed in
 | 
				
			||||||
 | 
							IsActive:    true,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return user, models.CreateUser(user)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										25
									
								
								services/auth/source/smtp/assert_interface_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								services/auth/source/smtp/assert_interface_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,25 @@
 | 
				
			|||||||
 | 
					// Copyright 2021 The Gitea Authors. All rights reserved.
 | 
				
			||||||
 | 
					// Use of this source code is governed by a MIT-style
 | 
				
			||||||
 | 
					// license that can be found in the LICENSE file.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package smtp_test
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/models"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/services/auth"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/services/auth/source/smtp"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// This test file exists to assert that our Source exposes the interfaces that we expect
 | 
				
			||||||
 | 
					// It tightly binds the interfaces and implementation without breaking go import cycles
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type sourceInterface interface {
 | 
				
			||||||
 | 
						auth.PasswordAuthenticator
 | 
				
			||||||
 | 
						models.LoginConfig
 | 
				
			||||||
 | 
						models.SkipVerifiable
 | 
				
			||||||
 | 
						models.HasTLSer
 | 
				
			||||||
 | 
						models.UseTLSer
 | 
				
			||||||
 | 
						models.LoginSourceSettable
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var _ (sourceInterface) = &smtp.Source{}
 | 
				
			||||||
							
								
								
									
										81
									
								
								services/auth/source/smtp/auth.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								services/auth/source/smtp/auth.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,81 @@
 | 
				
			|||||||
 | 
					// Copyright 2021 The Gitea Authors. All rights reserved.
 | 
				
			||||||
 | 
					// Use of this source code is governed by a MIT-style
 | 
				
			||||||
 | 
					// license that can be found in the LICENSE file.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package smtp
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"crypto/tls"
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"net/smtp"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/models"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//   _________   __________________________
 | 
				
			||||||
 | 
					//  /   _____/  /     \__    ___/\______   \
 | 
				
			||||||
 | 
					//  \_____  \  /  \ /  \|    |    |     ___/
 | 
				
			||||||
 | 
					//  /        \/    Y    \    |    |    |
 | 
				
			||||||
 | 
					// /_______  /\____|__  /____|    |____|
 | 
				
			||||||
 | 
					//         \/         \/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type loginAuthenticator struct {
 | 
				
			||||||
 | 
						username, password string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (auth *loginAuthenticator) Start(server *smtp.ServerInfo) (string, []byte, error) {
 | 
				
			||||||
 | 
						return "LOGIN", []byte(auth.username), nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (auth *loginAuthenticator) Next(fromServer []byte, more bool) ([]byte, error) {
 | 
				
			||||||
 | 
						if more {
 | 
				
			||||||
 | 
							switch string(fromServer) {
 | 
				
			||||||
 | 
							case "Username:":
 | 
				
			||||||
 | 
								return []byte(auth.username), nil
 | 
				
			||||||
 | 
							case "Password:":
 | 
				
			||||||
 | 
								return []byte(auth.password), nil
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// SMTP authentication type names.
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						PlainAuthentication = "PLAIN"
 | 
				
			||||||
 | 
						LoginAuthentication = "LOGIN"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Authenticators contains available SMTP authentication type names.
 | 
				
			||||||
 | 
					var Authenticators = []string{PlainAuthentication, LoginAuthentication}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Authenticate performs an SMTP authentication.
 | 
				
			||||||
 | 
					func Authenticate(a smtp.Auth, source *Source) error {
 | 
				
			||||||
 | 
						c, err := smtp.Dial(fmt.Sprintf("%s:%d", source.Host, source.Port))
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						defer c.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err = c.Hello("gogs"); err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if source.TLS {
 | 
				
			||||||
 | 
							if ok, _ := c.Extension("STARTTLS"); ok {
 | 
				
			||||||
 | 
								if err = c.StartTLS(&tls.Config{
 | 
				
			||||||
 | 
									InsecureSkipVerify: source.SkipVerify,
 | 
				
			||||||
 | 
									ServerName:         source.Host,
 | 
				
			||||||
 | 
								}); err != nil {
 | 
				
			||||||
 | 
									return err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								return errors.New("SMTP server unsupports TLS")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if ok, _ := c.Extension("AUTH"); ok {
 | 
				
			||||||
 | 
							return c.Auth(a)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return models.ErrUnsupportedLoginType
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										66
									
								
								services/auth/source/smtp/source.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								services/auth/source/smtp/source.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,66 @@
 | 
				
			|||||||
 | 
					// Copyright 2021 The Gitea Authors. All rights reserved.
 | 
				
			||||||
 | 
					// Use of this source code is governed by a MIT-style
 | 
				
			||||||
 | 
					// license that can be found in the LICENSE file.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package smtp
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/models"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						jsoniter "github.com/json-iterator/go"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//   _________   __________________________
 | 
				
			||||||
 | 
					//  /   _____/  /     \__    ___/\______   \
 | 
				
			||||||
 | 
					//  \_____  \  /  \ /  \|    |    |     ___/
 | 
				
			||||||
 | 
					//  /        \/    Y    \    |    |    |
 | 
				
			||||||
 | 
					// /_______  /\____|__  /____|    |____|
 | 
				
			||||||
 | 
					//         \/         \/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Source holds configuration for the SMTP login source.
 | 
				
			||||||
 | 
					type Source struct {
 | 
				
			||||||
 | 
						Auth           string
 | 
				
			||||||
 | 
						Host           string
 | 
				
			||||||
 | 
						Port           int
 | 
				
			||||||
 | 
						AllowedDomains string `xorm:"TEXT"`
 | 
				
			||||||
 | 
						TLS            bool
 | 
				
			||||||
 | 
						SkipVerify     bool
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// reference to the loginSource
 | 
				
			||||||
 | 
						loginSource *models.LoginSource
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// FromDB fills up an SMTPConfig from serialized format.
 | 
				
			||||||
 | 
					func (source *Source) FromDB(bs []byte) error {
 | 
				
			||||||
 | 
						return models.JSONUnmarshalHandleDoubleEncode(bs, &source)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ToDB exports an SMTPConfig to a serialized format.
 | 
				
			||||||
 | 
					func (source *Source) ToDB() ([]byte, error) {
 | 
				
			||||||
 | 
						json := jsoniter.ConfigCompatibleWithStandardLibrary
 | 
				
			||||||
 | 
						return json.Marshal(source)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// IsSkipVerify returns if SkipVerify is set
 | 
				
			||||||
 | 
					func (source *Source) IsSkipVerify() bool {
 | 
				
			||||||
 | 
						return source.SkipVerify
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// HasTLS returns true for SMTP
 | 
				
			||||||
 | 
					func (source *Source) HasTLS() bool {
 | 
				
			||||||
 | 
						return true
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// UseTLS returns if TLS is set
 | 
				
			||||||
 | 
					func (source *Source) UseTLS() bool {
 | 
				
			||||||
 | 
						return source.TLS
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// SetLoginSource sets the related LoginSource
 | 
				
			||||||
 | 
					func (source *Source) SetLoginSource(loginSource *models.LoginSource) {
 | 
				
			||||||
 | 
						source.loginSource = loginSource
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func init() {
 | 
				
			||||||
 | 
						models.RegisterLoginTypeConfig(models.LoginSMTP, &Source{})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										71
									
								
								services/auth/source/smtp/source_authenticate.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								services/auth/source/smtp/source_authenticate.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,71 @@
 | 
				
			|||||||
 | 
					// Copyright 2021 The Gitea Authors. All rights reserved.
 | 
				
			||||||
 | 
					// Use of this source code is governed by a MIT-style
 | 
				
			||||||
 | 
					// license that can be found in the LICENSE file.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package smtp
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
						"net/smtp"
 | 
				
			||||||
 | 
						"net/textproto"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/models"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/util"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Authenticate queries if the provided login/password is authenticates against the SMTP server
 | 
				
			||||||
 | 
					// Users will be autoregistered as required
 | 
				
			||||||
 | 
					func (source *Source) Authenticate(user *models.User, login, password string) (*models.User, error) {
 | 
				
			||||||
 | 
						// Verify allowed domains.
 | 
				
			||||||
 | 
						if len(source.AllowedDomains) > 0 {
 | 
				
			||||||
 | 
							idx := strings.Index(login, "@")
 | 
				
			||||||
 | 
							if idx == -1 {
 | 
				
			||||||
 | 
								return nil, models.ErrUserNotExist{Name: login}
 | 
				
			||||||
 | 
							} else if !util.IsStringInSlice(login[idx+1:], strings.Split(source.AllowedDomains, ","), true) {
 | 
				
			||||||
 | 
								return nil, models.ErrUserNotExist{Name: login}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var auth smtp.Auth
 | 
				
			||||||
 | 
						if source.Auth == PlainAuthentication {
 | 
				
			||||||
 | 
							auth = smtp.PlainAuth("", login, password, source.Host)
 | 
				
			||||||
 | 
						} else if source.Auth == LoginAuthentication {
 | 
				
			||||||
 | 
							auth = &loginAuthenticator{login, password}
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							return nil, errors.New("Unsupported SMTP auth type")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := Authenticate(auth, source); err != nil {
 | 
				
			||||||
 | 
							// Check standard error format first,
 | 
				
			||||||
 | 
							// then fallback to worse case.
 | 
				
			||||||
 | 
							tperr, ok := err.(*textproto.Error)
 | 
				
			||||||
 | 
							if (ok && tperr.Code == 535) ||
 | 
				
			||||||
 | 
								strings.Contains(err.Error(), "Username and Password not accepted") {
 | 
				
			||||||
 | 
								return nil, models.ErrUserNotExist{Name: login}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if user != nil {
 | 
				
			||||||
 | 
							return user, nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						username := login
 | 
				
			||||||
 | 
						idx := strings.Index(login, "@")
 | 
				
			||||||
 | 
						if idx > -1 {
 | 
				
			||||||
 | 
							username = login[:idx]
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						user = &models.User{
 | 
				
			||||||
 | 
							LowerName:   strings.ToLower(username),
 | 
				
			||||||
 | 
							Name:        strings.ToLower(username),
 | 
				
			||||||
 | 
							Email:       login,
 | 
				
			||||||
 | 
							Passwd:      password,
 | 
				
			||||||
 | 
							LoginType:   models.LoginSMTP,
 | 
				
			||||||
 | 
							LoginSource: source.loginSource.ID,
 | 
				
			||||||
 | 
							LoginName:   login,
 | 
				
			||||||
 | 
							IsActive:    true,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return user, models.CreateUser(user)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										19
									
								
								services/auth/source/sspi/assert_interface_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								services/auth/source/sspi/assert_interface_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
				
			|||||||
 | 
					// Copyright 2021 The Gitea Authors. All rights reserved.
 | 
				
			||||||
 | 
					// Use of this source code is governed by a MIT-style
 | 
				
			||||||
 | 
					// license that can be found in the LICENSE file.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package sspi_test
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/models"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/services/auth/source/sspi"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// This test file exists to assert that our Source exposes the interfaces that we expect
 | 
				
			||||||
 | 
					// It tightly binds the interfaces and implementation without breaking go import cycles
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type sourceInterface interface {
 | 
				
			||||||
 | 
						models.LoginConfig
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var _ (sourceInterface) = &sspi.Source{}
 | 
				
			||||||
							
								
								
									
										41
									
								
								services/auth/source/sspi/source.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								services/auth/source/sspi/source.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,41 @@
 | 
				
			|||||||
 | 
					// Copyright 2021 The Gitea Authors. All rights reserved.
 | 
				
			||||||
 | 
					// Use of this source code is governed by a MIT-style
 | 
				
			||||||
 | 
					// license that can be found in the LICENSE file.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package sspi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/models"
 | 
				
			||||||
 | 
						jsoniter "github.com/json-iterator/go"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//   _________ ___________________.___
 | 
				
			||||||
 | 
					//  /   _____//   _____/\______   \   |
 | 
				
			||||||
 | 
					//  \_____  \ \_____  \  |     ___/   |
 | 
				
			||||||
 | 
					//  /        \/        \ |    |   |   |
 | 
				
			||||||
 | 
					// /_______  /_______  / |____|   |___|
 | 
				
			||||||
 | 
					//         \/        \/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Source holds configuration for SSPI single sign-on.
 | 
				
			||||||
 | 
					type Source struct {
 | 
				
			||||||
 | 
						AutoCreateUsers      bool
 | 
				
			||||||
 | 
						AutoActivateUsers    bool
 | 
				
			||||||
 | 
						StripDomainNames     bool
 | 
				
			||||||
 | 
						SeparatorReplacement string
 | 
				
			||||||
 | 
						DefaultLanguage      string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// FromDB fills up an SSPIConfig from serialized format.
 | 
				
			||||||
 | 
					func (cfg *Source) FromDB(bs []byte) error {
 | 
				
			||||||
 | 
						return models.JSONUnmarshalHandleDoubleEncode(bs, &cfg)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ToDB exports an SSPIConfig to a serialized format.
 | 
				
			||||||
 | 
					func (cfg *Source) ToDB() ([]byte, error) {
 | 
				
			||||||
 | 
						json := jsoniter.ConfigCompatibleWithStandardLibrary
 | 
				
			||||||
 | 
						return json.Marshal(cfg)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func init() {
 | 
				
			||||||
 | 
						models.RegisterLoginTypeConfig(models.LoginSSPI, &Source{})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -15,6 +15,7 @@ import (
 | 
				
			|||||||
	"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"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/services/auth/source/sspi"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	gouuid "github.com/google/uuid"
 | 
						gouuid "github.com/google/uuid"
 | 
				
			||||||
	"github.com/quasoft/websspi"
 | 
						"github.com/quasoft/websspi"
 | 
				
			||||||
@@ -32,7 +33,10 @@ var (
 | 
				
			|||||||
	sspiAuth *websspi.Authenticator
 | 
						sspiAuth *websspi.Authenticator
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Ensure the struct implements the interface.
 | 
						// Ensure the struct implements the interface.
 | 
				
			||||||
	_ Auth = &SSPI{}
 | 
						_ Method        = &SSPI{}
 | 
				
			||||||
 | 
						_ Named         = &SSPI{}
 | 
				
			||||||
 | 
						_ Initializable = &SSPI{}
 | 
				
			||||||
 | 
						_ Freeable      = &SSPI{}
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// SSPI implements the SingleSignOn interface and authenticates requests
 | 
					// SSPI implements the SingleSignOn interface and authenticates requests
 | 
				
			||||||
@@ -146,7 +150,7 @@ func (s *SSPI) Verify(req *http.Request, w http.ResponseWriter, store DataStore,
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// getConfig retrieves the SSPI configuration from login sources
 | 
					// getConfig retrieves the SSPI configuration from login sources
 | 
				
			||||||
func (s *SSPI) getConfig() (*models.SSPIConfig, error) {
 | 
					func (s *SSPI) getConfig() (*sspi.Source, error) {
 | 
				
			||||||
	sources, err := models.ActiveLoginSources(models.LoginSSPI)
 | 
						sources, err := models.ActiveLoginSources(models.LoginSSPI)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return nil, err
 | 
							return nil, err
 | 
				
			||||||
@@ -157,7 +161,7 @@ func (s *SSPI) getConfig() (*models.SSPIConfig, error) {
 | 
				
			|||||||
	if len(sources) > 1 {
 | 
						if len(sources) > 1 {
 | 
				
			||||||
		return nil, errors.New("more than one active login source of type SSPI found")
 | 
							return nil, errors.New("more than one active login source of type SSPI found")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	return sources[0].SSPI(), nil
 | 
						return sources[0].Cfg.(*sspi.Source), nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (s *SSPI) shouldAuthenticate(req *http.Request) (shouldAuth bool) {
 | 
					func (s *SSPI) shouldAuthenticate(req *http.Request) (shouldAuth bool) {
 | 
				
			||||||
@@ -177,7 +181,7 @@ func (s *SSPI) shouldAuthenticate(req *http.Request) (shouldAuth bool) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// newUser creates a new user object for the purpose of automatic registration
 | 
					// newUser creates a new user object for the purpose of automatic registration
 | 
				
			||||||
// and populates its name and email with the information present in request headers.
 | 
					// and populates its name and email with the information present in request headers.
 | 
				
			||||||
func (s *SSPI) newUser(username string, cfg *models.SSPIConfig) (*models.User, error) {
 | 
					func (s *SSPI) newUser(username string, cfg *sspi.Source) (*models.User, error) {
 | 
				
			||||||
	email := gouuid.New().String() + "@localhost.localdomain"
 | 
						email := gouuid.New().String() + "@localhost.localdomain"
 | 
				
			||||||
	user := &models.User{
 | 
						user := &models.User{
 | 
				
			||||||
		Name:                         username,
 | 
							Name:                         username,
 | 
				
			||||||
@@ -214,7 +218,7 @@ func stripDomainNames(username string) string {
 | 
				
			|||||||
	return username
 | 
						return username
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func replaceSeparators(username string, cfg *models.SSPIConfig) string {
 | 
					func replaceSeparators(username string, cfg *sspi.Source) string {
 | 
				
			||||||
	newSep := cfg.SeparatorReplacement
 | 
						newSep := cfg.SeparatorReplacement
 | 
				
			||||||
	username = strings.ReplaceAll(username, "\\", newSep)
 | 
						username = strings.ReplaceAll(username, "\\", newSep)
 | 
				
			||||||
	username = strings.ReplaceAll(username, "/", newSep)
 | 
						username = strings.ReplaceAll(username, "/", newSep)
 | 
				
			||||||
@@ -222,7 +226,7 @@ func replaceSeparators(username string, cfg *models.SSPIConfig) string {
 | 
				
			|||||||
	return username
 | 
						return username
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func sanitizeUsername(username string, cfg *models.SSPIConfig) string {
 | 
					func sanitizeUsername(username string, cfg *sspi.Source) string {
 | 
				
			||||||
	if len(username) == 0 {
 | 
						if len(username) == 0 {
 | 
				
			||||||
		return ""
 | 
							return ""
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										43
									
								
								services/auth/sync.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								services/auth/sync.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,43 @@
 | 
				
			|||||||
 | 
					// Copyright 2021 The Gitea Authors. All rights reserved.
 | 
				
			||||||
 | 
					// Use of this source code is governed by a MIT-style
 | 
				
			||||||
 | 
					// license that can be found in the LICENSE file.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package auth
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/models"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/log"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// SyncExternalUsers is used to synchronize users with external authorization source
 | 
				
			||||||
 | 
					func SyncExternalUsers(ctx context.Context, updateExisting bool) error {
 | 
				
			||||||
 | 
						log.Trace("Doing: SyncExternalUsers")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ls, err := models.LoginSources()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							log.Error("SyncExternalUsers: %v", err)
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, s := range ls {
 | 
				
			||||||
 | 
							if !s.IsActive || !s.IsSyncEnabled {
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							select {
 | 
				
			||||||
 | 
							case <-ctx.Done():
 | 
				
			||||||
 | 
								log.Warn("SyncExternalUsers: Cancelled before update of %s", s.Name)
 | 
				
			||||||
 | 
								return models.ErrCancelledf("Before update of %s", s.Name)
 | 
				
			||||||
 | 
							default:
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if syncable, ok := s.Cfg.(SynchronizableSource); ok {
 | 
				
			||||||
 | 
								err := syncable.Sync(ctx, updateExisting)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									return err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -22,7 +22,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
				<!-- LDAP and DLDAP -->
 | 
									<!-- LDAP and DLDAP -->
 | 
				
			||||||
				{{if or .Source.IsLDAP .Source.IsDLDAP}}
 | 
									{{if or .Source.IsLDAP .Source.IsDLDAP}}
 | 
				
			||||||
					{{ $cfg:=.Source.LDAP }}
 | 
										{{ $cfg:=.Source.Cfg }}
 | 
				
			||||||
					<div class="inline required field {{if .Err_SecurityProtocol}}error{{end}}">
 | 
										<div class="inline required field {{if .Err_SecurityProtocol}}error{{end}}">
 | 
				
			||||||
						<label>{{.i18n.Tr "admin.auths.security_protocol"}}</label>
 | 
											<label>{{.i18n.Tr "admin.auths.security_protocol"}}</label>
 | 
				
			||||||
						<div class="ui selection security-protocol dropdown">
 | 
											<div class="ui selection security-protocol dropdown">
 | 
				
			||||||
@@ -151,7 +151,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
				<!-- SMTP -->
 | 
									<!-- SMTP -->
 | 
				
			||||||
				{{if .Source.IsSMTP}}
 | 
									{{if .Source.IsSMTP}}
 | 
				
			||||||
					{{ $cfg:=.Source.SMTP }}
 | 
										{{ $cfg:=.Source.Cfg }}
 | 
				
			||||||
					<div class="inline required field">
 | 
										<div class="inline required field">
 | 
				
			||||||
						<label>{{.i18n.Tr "admin.auths.smtp_auth"}}</label>
 | 
											<label>{{.i18n.Tr "admin.auths.smtp_auth"}}</label>
 | 
				
			||||||
						<div class="ui selection type dropdown">
 | 
											<div class="ui selection type dropdown">
 | 
				
			||||||
@@ -182,7 +182,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
				<!-- PAM -->
 | 
									<!-- PAM -->
 | 
				
			||||||
				{{if .Source.IsPAM}}
 | 
									{{if .Source.IsPAM}}
 | 
				
			||||||
					{{ $cfg:=.Source.PAM }}
 | 
										{{ $cfg:=.Source.Cfg }}
 | 
				
			||||||
					<div class="required field">
 | 
										<div class="required field">
 | 
				
			||||||
						<label for="pam_service_name">{{.i18n.Tr "admin.auths.pam_service_name"}}</label>
 | 
											<label for="pam_service_name">{{.i18n.Tr "admin.auths.pam_service_name"}}</label>
 | 
				
			||||||
						<input id="pam_service_name" name="pam_service_name" value="{{$cfg.ServiceName}}" required>
 | 
											<input id="pam_service_name" name="pam_service_name" value="{{$cfg.ServiceName}}" required>
 | 
				
			||||||
@@ -195,7 +195,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
				<!-- OAuth2 -->
 | 
									<!-- OAuth2 -->
 | 
				
			||||||
				{{if .Source.IsOAuth2}}
 | 
									{{if .Source.IsOAuth2}}
 | 
				
			||||||
					{{ $cfg:=.Source.OAuth2 }}
 | 
										{{ $cfg:=.Source.Cfg }}
 | 
				
			||||||
					<div class="inline required field">
 | 
										<div class="inline required field">
 | 
				
			||||||
						<label>{{.i18n.Tr "admin.auths.oauth2_provider"}}</label>
 | 
											<label>{{.i18n.Tr "admin.auths.oauth2_provider"}}</label>
 | 
				
			||||||
						<div class="ui selection type dropdown">
 | 
											<div class="ui selection type dropdown">
 | 
				
			||||||
@@ -258,7 +258,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
				<!-- SSPI -->
 | 
									<!-- SSPI -->
 | 
				
			||||||
				{{if .Source.IsSSPI}}
 | 
									{{if .Source.IsSSPI}}
 | 
				
			||||||
					{{ $cfg:=.Source.SSPI }}
 | 
										{{ $cfg:=.Source.Cfg }}
 | 
				
			||||||
					<div class="field">
 | 
										<div class="field">
 | 
				
			||||||
						<div class="ui checkbox">
 | 
											<div class="ui checkbox">
 | 
				
			||||||
							<label for="sspi_auto_create_users"><strong>{{.i18n.Tr "admin.auths.sspi_auto_create_users"}}</strong></label>
 | 
												<label for="sspi_auto_create_users"><strong>{{.i18n.Tr "admin.auths.sspi_auto_create_users"}}</strong></label>
 | 
				
			||||||
@@ -325,7 +325,7 @@
 | 
				
			|||||||
				<div class="inline field">
 | 
									<div class="inline field">
 | 
				
			||||||
					<div class="ui checkbox">
 | 
										<div class="ui checkbox">
 | 
				
			||||||
						<label><strong>{{.i18n.Tr "admin.auths.activated"}}</strong></label>
 | 
											<label><strong>{{.i18n.Tr "admin.auths.activated"}}</strong></label>
 | 
				
			||||||
						<input name="is_active" type="checkbox" {{if .Source.IsActived}}checked{{end}}>
 | 
											<input name="is_active" type="checkbox" {{if .Source.IsActive}}checked{{end}}>
 | 
				
			||||||
					</div>
 | 
										</div>
 | 
				
			||||||
				</div>
 | 
									</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -28,7 +28,7 @@
 | 
				
			|||||||
							<td>{{.ID}}</td>
 | 
												<td>{{.ID}}</td>
 | 
				
			||||||
							<td><a href="{{AppSubUrl}}/admin/auths/{{.ID}}">{{.Name}}</a></td>
 | 
												<td><a href="{{AppSubUrl}}/admin/auths/{{.ID}}">{{.Name}}</a></td>
 | 
				
			||||||
							<td>{{.TypeName}}</td>
 | 
												<td>{{.TypeName}}</td>
 | 
				
			||||||
							<td>{{if .IsActived}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}</td>
 | 
												<td>{{if .IsActive}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}</td>
 | 
				
			||||||
							<td><span class="poping up" data-content="{{.UpdatedUnix.FormatShort}}" data-variation="tiny">{{.UpdatedUnix.FormatShort}}</span></td>
 | 
												<td><span class="poping up" data-content="{{.UpdatedUnix.FormatShort}}" data-variation="tiny">{{.UpdatedUnix.FormatShort}}</span></td>
 | 
				
			||||||
							<td><span class="poping up" data-content="{{.CreatedUnix.FormatLong}}" data-variation="tiny">{{.CreatedUnix.FormatShort}}</span></td>
 | 
												<td><span class="poping up" data-content="{{.CreatedUnix.FormatLong}}" data-variation="tiny">{{.CreatedUnix.FormatShort}}</span></td>
 | 
				
			||||||
							<td><a href="{{AppSubUrl}}/admin/auths/{{.ID}}">{{svg "octicon-pencil"}}</a></td>
 | 
												<td><a href="{{AppSubUrl}}/admin/auths/{{.ID}}">{{svg "octicon-pencil"}}</a></td>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -16,7 +16,7 @@
 | 
				
			|||||||
				</div>
 | 
									</div>
 | 
				
			||||||
					<div class="content">
 | 
										<div class="content">
 | 
				
			||||||
						<strong>{{$provider}}</strong>
 | 
											<strong>{{$provider}}</strong>
 | 
				
			||||||
						{{if $loginSource.IsActived}}<span class="text red">{{$.i18n.Tr "settings.active"}}</span>{{end}}
 | 
											{{if $loginSource.IsActive}}<span class="text red">{{$.i18n.Tr "settings.active"}}</span>{{end}}
 | 
				
			||||||
					</div>
 | 
										</div>
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
		{{end}}
 | 
							{{end}}
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user