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:
zeripath 2021-07-24 11:16:34 +01:00 committed by GitHub
parent f135a818f5
commit 5d2e11eedb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
77 changed files with 3803 additions and 2951 deletions

View file

@ -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"),
@ -625,10 +625,10 @@ 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()

View file

@ -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
} }
@ -289,17 +289,15 @@ 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
} }
@ -336,17 +334,15 @@ 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
} }

View file

@ -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,30 +54,28 @@ 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, SecurityProtocol: ldap.SecurityProtocol(1),
SecurityProtocol: ldap.SecurityProtocol(1), SkipVerify: true,
SkipVerify: true, BindDN: "cn=readonly,dc=full-domain-bind,dc=org",
BindDN: "cn=readonly,dc=full-domain-bind,dc=org", BindPassword: "secret-bind-full",
BindPassword: "secret-bind-full", UserBase: "ou=Users,dc=full-domain-bind,dc=org",
UserBase: "ou=Users,dc=full-domain-bind,dc=org", AttributeUsername: "uid-bind full",
AttributeUsername: "uid-bind full", AttributeName: "givenName-bind full",
AttributeName: "givenName-bind full", AttributeSurname: "sn-bind full",
AttributeSurname: "sn-bind full", AttributeMail: "mail-bind full",
AttributeMail: "mail-bind full", AttributesInBind: true,
AttributesInBind: true, AttributeSSHPublicKey: "publickey-bind full",
AttributeSSHPublicKey: "publickey-bind full", SearchPageSize: 99,
SearchPageSize: 99, Filter: "(memberOf=cn=user-group,ou=example,dc=full-domain-bind,dc=org)",
Filter: "(memberOf=cn=user-group,ou=example,dc=full-domain-bind,dc=org)", AdminFilter: "(memberOf=cn=admin-group,ou=example,dc=full-domain-bind,dc=org)",
AdminFilter: "(memberOf=cn=admin-group,ou=example,dc=full-domain-bind,dc=org)", RestrictedFilter: "(memberOf=cn=restricted-group,ou=example,dc=full-domain-bind,dc=org)",
RestrictedFilter: "(memberOf=cn=restricted-group,ou=example,dc=full-domain-bind,dc=org)", Enabled: true,
Enabled: true,
},
}, },
}, },
}, },
@ -94,20 +92,18 @@ func TestAddLdapBindDn(t *testing.T) {
"--email-attribute", "mail-bind min", "--email-attribute", "mail-bind min",
}, },
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, SecurityProtocol: ldap.SecurityProtocol(0),
SecurityProtocol: ldap.SecurityProtocol(0), UserBase: "ou=Users,dc=min-domain-bind,dc=org",
UserBase: "ou=Users,dc=min-domain-bind,dc=org", AttributeMail: "mail-bind min",
AttributeMail: "mail-bind min", Filter: "(memberOf=cn=user-group,ou=example,dc=min-domain-bind,dc=org)",
Filter: "(memberOf=cn=user-group,ou=example,dc=min-domain-bind,dc=org)", Enabled: true,
Enabled: true,
},
}, },
}, },
}, },
@ -276,28 +272,26 @@ func TestAddLdapSimpleAuth(t *testing.T) {
"--user-dn", "cn=%s,ou=Users,dc=full-domain-simple,dc=org", "--user-dn", "cn=%s,ou=Users,dc=full-domain-simple,dc=org",
}, },
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, SecurityProtocol: ldap.SecurityProtocol(2),
SecurityProtocol: ldap.SecurityProtocol(2), SkipVerify: true,
SkipVerify: true, UserDN: "cn=%s,ou=Users,dc=full-domain-simple,dc=org",
UserDN: "cn=%s,ou=Users,dc=full-domain-simple,dc=org", UserBase: "ou=Users,dc=full-domain-simple,dc=org",
UserBase: "ou=Users,dc=full-domain-simple,dc=org", AttributeUsername: "uid-simple full",
AttributeUsername: "uid-simple full", AttributeName: "givenName-simple full",
AttributeName: "givenName-simple full", AttributeSurname: "sn-simple full",
AttributeSurname: "sn-simple full", AttributeMail: "mail-simple full",
AttributeMail: "mail-simple full", AttributeSSHPublicKey: "publickey-simple full",
AttributeSSHPublicKey: "publickey-simple full", Filter: "(&(objectClass=posixAccount)(full-simple-cn=%s))",
Filter: "(&(objectClass=posixAccount)(full-simple-cn=%s))", AdminFilter: "(memberOf=cn=admin-group,ou=example,dc=full-domain-simple,dc=org)",
AdminFilter: "(memberOf=cn=admin-group,ou=example,dc=full-domain-simple,dc=org)", RestrictedFilter: "(memberOf=cn=restricted-group,ou=example,dc=full-domain-simple,dc=org)",
RestrictedFilter: "(memberOf=cn=restricted-group,ou=example,dc=full-domain-simple,dc=org)", Enabled: true,
Enabled: true,
},
}, },
}, },
}, },
@ -314,20 +308,18 @@ func TestAddLdapSimpleAuth(t *testing.T) {
"--user-dn", "cn=%s,ou=Users,dc=min-domain-simple,dc=org", "--user-dn", "cn=%s,ou=Users,dc=min-domain-simple,dc=org",
}, },
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, SecurityProtocol: ldap.SecurityProtocol(0),
SecurityProtocol: ldap.SecurityProtocol(0), UserDN: "cn=%s,ou=Users,dc=min-domain-simple,dc=org",
UserDN: "cn=%s,ou=Users,dc=min-domain-simple,dc=org", AttributeMail: "mail-simple min",
AttributeMail: "mail-simple min", Filter: "(&(objectClass=posixAccount)(min-simple-cn=%s))",
Filter: "(&(objectClass=posixAccount)(min-simple-cn=%s))", Enabled: true,
Enabled: true,
},
}, },
}, },
}, },
@ -516,41 +508,37 @@ 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, SecurityProtocol: ldap.SecurityProtocol(1),
SecurityProtocol: ldap.SecurityProtocol(1), SkipVerify: true,
SkipVerify: true, BindDN: "cn=readonly,dc=full-domain-bind,dc=org",
BindDN: "cn=readonly,dc=full-domain-bind,dc=org", BindPassword: "secret-bind-full",
BindPassword: "secret-bind-full", UserBase: "ou=Users,dc=full-domain-bind,dc=org",
UserBase: "ou=Users,dc=full-domain-bind,dc=org", AttributeUsername: "uid-bind full",
AttributeUsername: "uid-bind full", AttributeName: "givenName-bind full",
AttributeName: "givenName-bind full", AttributeSurname: "sn-bind full",
AttributeSurname: "sn-bind full", AttributeMail: "mail-bind full",
AttributeMail: "mail-bind full", AttributesInBind: false,
AttributesInBind: false, AttributeSSHPublicKey: "publickey-bind full",
AttributeSSHPublicKey: "publickey-bind full", SearchPageSize: 99,
SearchPageSize: 99, Filter: "(memberOf=cn=user-group,ou=example,dc=full-domain-bind,dc=org)",
Filter: "(memberOf=cn=user-group,ou=example,dc=full-domain-bind,dc=org)", AdminFilter: "(memberOf=cn=admin-group,ou=example,dc=full-domain-bind,dc=org)",
AdminFilter: "(memberOf=cn=admin-group,ou=example,dc=full-domain-bind,dc=org)", RestrictedFilter: "(memberOf=cn=restricted-group,ou=example,dc=full-domain-bind,dc=org)",
RestrictedFilter: "(memberOf=cn=restricted-group,ou=example,dc=full-domain-bind,dc=org)", Enabled: true,
Enabled: true,
},
}, },
}, },
}, },
@ -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,10 +563,8 @@ 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",
},
}, },
}, },
}, },
@ -592,18 +576,14 @@ func TestUpdateLdapBindDn(t *testing.T) {
"--not-active", "--not-active",
}, },
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,10 +595,8 @@ 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),
},
}, },
}, },
}, },
@ -631,10 +609,8 @@ 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,
},
}, },
}, },
}, },
@ -647,10 +623,8 @@ 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",
},
}, },
}, },
}, },
@ -663,10 +637,8 @@ 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,
},
}, },
}, },
}, },
@ -679,10 +651,8 @@ 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",
},
}, },
}, },
}, },
@ -695,10 +665,8 @@ 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)",
},
}, },
}, },
}, },
@ -711,10 +679,8 @@ 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)",
},
}, },
}, },
}, },
@ -727,10 +693,8 @@ 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",
},
}, },
}, },
}, },
@ -743,10 +707,8 @@ 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",
},
}, },
}, },
}, },
@ -759,10 +721,8 @@ 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",
},
}, },
}, },
}, },
@ -775,10 +735,8 @@ 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",
},
}, },
}, },
}, },
@ -791,10 +749,8 @@ 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,
},
}, },
}, },
}, },
@ -807,10 +763,8 @@ 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",
},
}, },
}, },
}, },
@ -823,10 +777,8 @@ 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",
},
}, },
}, },
}, },
@ -839,10 +791,8 @@ 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",
},
}, },
}, },
}, },
@ -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,10 +818,8 @@ 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,
},
}, },
}, },
}, },
@ -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
}, },
} }
@ -994,27 +936,25 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
}, },
id: 7, id: 7,
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, SecurityProtocol: ldap.SecurityProtocol(2),
SecurityProtocol: ldap.SecurityProtocol(2), SkipVerify: true,
SkipVerify: true, UserDN: "cn=%s,ou=Users,dc=full-domain-simple,dc=org",
UserDN: "cn=%s,ou=Users,dc=full-domain-simple,dc=org", UserBase: "ou=Users,dc=full-domain-simple,dc=org",
UserBase: "ou=Users,dc=full-domain-simple,dc=org", AttributeUsername: "uid-simple full",
AttributeUsername: "uid-simple full", AttributeName: "givenName-simple full",
AttributeName: "givenName-simple full", AttributeSurname: "sn-simple full",
AttributeSurname: "sn-simple full", AttributeMail: "mail-simple full",
AttributeMail: "mail-simple full", AttributeSSHPublicKey: "publickey-simple full",
AttributeSSHPublicKey: "publickey-simple full", Filter: "(&(objectClass=posixAccount)(full-simple-cn=%s))",
Filter: "(&(objectClass=posixAccount)(full-simple-cn=%s))", AdminFilter: "(memberOf=cn=admin-group,ou=example,dc=full-domain-simple,dc=org)",
AdminFilter: "(memberOf=cn=admin-group,ou=example,dc=full-domain-simple,dc=org)", RestrictedFilter: "(memberOf=cn=restricted-group,ou=example,dc=full-domain-simple,dc=org)",
RestrictedFilter: "(memberOf=cn=restricted-group,ou=example,dc=full-domain-simple,dc=org)",
},
}, },
}, },
}, },
@ -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,10 +979,8 @@ 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",
},
}, },
}, },
}, },
@ -1056,18 +992,14 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
"--not-active", "--not-active",
}, },
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,10 +1011,8 @@ 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),
},
}, },
}, },
}, },
@ -1095,10 +1025,8 @@ 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,
},
}, },
}, },
}, },
@ -1111,10 +1039,8 @@ 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",
},
}, },
}, },
}, },
@ -1127,10 +1053,8 @@ 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,
},
}, },
}, },
}, },
@ -1143,10 +1067,8 @@ 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",
},
}, },
}, },
}, },
@ -1159,10 +1081,8 @@ 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))",
},
}, },
}, },
}, },
@ -1175,10 +1095,8 @@ 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)",
},
}, },
}, },
}, },
@ -1191,10 +1109,8 @@ 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",
},
}, },
}, },
}, },
@ -1207,10 +1123,8 @@ 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",
},
}, },
}, },
}, },
@ -1223,10 +1137,8 @@ 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",
},
}, },
}, },
}, },
@ -1239,10 +1151,9 @@ 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",
},
}, },
}, },
}, },
@ -1255,10 +1166,8 @@ 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",
},
}, },
}, },
}, },
@ -1271,10 +1180,8 @@ 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",
},
}, },
}, },
}, },
@ -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
}, },
} }

View file

@ -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 {

View file

@ -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
}

View file

@ -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 // HasTLSer configurations provide a HasTLS to check if TLS can be enabled
// possible that a Blob may be double encoded or gain an unwanted prefix of 0xff 0xfe. type HasTLSer interface {
func jsonUnmarshalHandleDoubleEncode(bs []byte, v interface{}) error { HasTLS() bool
json := jsoniter.ConfigCompatibleWithStandardLibrary }
err := json.Unmarshal(bs, v)
if err != nil { // UseTLSer configurations provide a HasTLS to check if TLS is enabled
ok := true type UseTLSer interface {
rs := []byte{} UseTLS() bool
temp := make([]byte, 2) }
for _, rn := range string(bs) {
if rn > 0xffff { // SSHKeyProvider configurations provide ProvidesSSHKeys to check if they provide SSHKeys
ok = false type SSHKeyProvider interface {
break ProvidesSSHKeys() bool
} }
binary.LittleEndian.PutUint16(temp, uint16(rn))
rs = append(rs, temp...) // RegisterableSource configurations provide RegisterSource which needs to be run on creation
} type RegisterableSource interface {
if ok { RegisterSource() error
if rs[0] == 0xff && rs[1] == 0xfe { UnregisterSource() error
rs = rs[2:] }
}
err = json.Unmarshal(rs, v) // LoginSourceSettable configurations can have their loginSource set on them
type LoginSourceSettable interface {
SetLoginSource(*LoginSource)
}
// RegisterLoginTypeConfig register a config for a provided type
func RegisterLoginTypeConfig(typ LoginType, exemplar LoginConfig) {
if reflect.TypeOf(exemplar).Kind() == reflect.Ptr {
// Pointer:
registeredLoginConfigs[typ] = func() LoginConfig {
return reflect.New(reflect.ValueOf(exemplar).Elem().Type()).Interface().(LoginConfig)
} }
return
} }
if err != nil && len(bs) > 2 && bs[0] == 0xff && bs[1] == 0xfe {
err = json.Unmarshal(bs[2:], v) // Not a Pointer
registeredLoginConfigs[typ] = func() LoginConfig {
return reflect.New(reflect.TypeOf(exemplar)).Elem().Interface().(LoginConfig)
} }
return err
} }
// LDAPConfig holds configuration for LDAP login source. var registeredLoginConfigs = map[LoginType]func() LoginConfig{}
type LDAPConfig struct {
*ldap.Source
}
// FromDB fills up a LDAPConfig from serialized format.
func (cfg *LDAPConfig) FromDB(bs []byte) error {
err := jsonUnmarshalHandleDoubleEncode(bs, &cfg)
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.
func (cfg *LDAPConfig) ToDB() ([]byte, error) {
var err error
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
// protocol.
func (cfg *LDAPConfig) SecurityProtocolName() string {
return SecurityProtocolNames[cfg.SecurityProtocol]
}
// SMTPConfig holds configuration for the SMTP login source.
type SMTPConfig struct {
Auth string
Host string
Port int
AllowedDomains string `xorm:"TEXT"`
TLS bool
SkipVerify bool
}
// FromDB fills up an SMTPConfig from serialized format.
func (cfg *SMTPConfig) FromDB(bs []byte) error {
return jsonUnmarshalHandleDoubleEncode(bs, cfg)
}
// ToDB exports an SMTPConfig to a serialized format.
func (cfg *SMTPConfig) ToDB() ([]byte, error) {
json := jsoniter.ConfigCompatibleWithStandardLibrary
return json.Marshal(cfg)
}
// PAMConfig holds configuration for the PAM login source.
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,16 +210,24 @@ func CreateLoginSource(source *LoginSource) error {
} }
_, err = x.Insert(source) _, err = x.Insert(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 err != nil { if !source.IsActive {
// remove the LoginSource in case of errors while registering OAuth2 providers return nil
if _, err := x.Delete(source); err != nil { }
log.Error("CreateLoginSource: Error while wrapOpenIDConnectInitializeError: %v", err)
} registerableSource, ok := source.Cfg.(RegisterableSource)
return err 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 {
log.Error("CreateLoginSource: Error while wrapOpenIDConnectInitializeError: %v", 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,16 +312,24 @@ 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 err != nil { if !source.IsActive {
// restore original values since we cannot update the provider it self return nil
if _, err := x.ID(source.ID).AllCols().Update(originalLoginSource); err != nil { }
log.Error("UpdateSource: Error while wrapOpenIDConnectInitializeError: %v", err)
} registerableSource, ok := source.Cfg.(RegisterableSource)
return err if !ok {
return nil
}
err = registerableSource.RegisterSource()
if err != nil {
// restore original values since we cannot update the provider it self
if _, err := x.ID(source.ID).AllCols().Update(originalLoginSource); err != nil {
log.Error("UpdateSource: Error while wrapOpenIDConnectInitializeError: %v", 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}
}

View file

@ -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}"

View file

@ -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

View file

@ -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
View 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()
}

View 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)
}
}
}

View file

@ -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
}

View file

@ -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())
}

View file

@ -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.

File diff suppressed because it is too large Load diff

View 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
}

View 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
View 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)
}

View 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
View 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
}

View 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
View 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
}

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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 {

View file

@ -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)
}) })
} }

View file

@ -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)
} }

View file

@ -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,45 +116,43 @@ 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, SecurityProtocol: ldap.SecurityProtocol(form.SecurityProtocol),
SecurityProtocol: ldap.SecurityProtocol(form.SecurityProtocol), SkipVerify: form.SkipVerify,
SkipVerify: form.SkipVerify, BindDN: form.BindDN,
BindDN: form.BindDN, UserDN: form.UserDN,
UserDN: form.UserDN, BindPassword: form.BindPassword,
BindPassword: form.BindPassword, UserBase: form.UserBase,
UserBase: form.UserBase, AttributeUsername: form.AttributeUsername,
AttributeUsername: form.AttributeUsername, AttributeName: form.AttributeName,
AttributeName: form.AttributeName, AttributeSurname: form.AttributeSurname,
AttributeSurname: form.AttributeSurname, AttributeMail: form.AttributeMail,
AttributeMail: form.AttributeMail, AttributesInBind: form.AttributesInBind,
AttributesInBind: form.AttributesInBind, AttributeSSHPublicKey: form.AttributeSSHPublicKey,
AttributeSSHPublicKey: form.AttributeSSHPublicKey, SearchPageSize: pageSize,
SearchPageSize: pageSize, Filter: form.Filter,
Filter: form.Filter, GroupsEnabled: form.GroupsEnabled,
GroupsEnabled: form.GroupsEnabled, GroupDN: form.GroupDN,
GroupDN: form.GroupDN, GroupFilter: form.GroupFilter,
GroupFilter: form.GroupFilter, GroupMemberUID: form.GroupMemberUID,
GroupMemberUID: form.GroupMemberUID, UserUID: form.UserUID,
UserUID: form.UserUID, AdminFilter: form.AdminFilter,
AdminFilter: form.AdminFilter, 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 {

View file

@ -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

View file

@ -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)

View file

@ -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,

View file

@ -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)

View file

@ -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
} }

View file

@ -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)
} }

View file

@ -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)

View file

@ -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
} }

View file

@ -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
}

View file

@ -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()

View file

@ -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

View file

@ -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
View 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}
}

View 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{}

View 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
}

View 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{})
}

View file

@ -1,5 +1,4 @@
Gitea LDAP Authentication Module # Gitea LDAP Authentication Module
===============================
## About ## About
@ -30,94 +29,94 @@ section in the admin panel. Both the LDAP via BindDN and the simple auth LDAP
share the following fields: share the following fields:
* Authorization Name **(required)** * Authorization Name **(required)**
* A name to assign to the new method of authorization. * A name to assign to the new method of authorization.
* Host **(required)** * Host **(required)**
* The address where the LDAP server can be reached. * The address where the LDAP server can be reached.
* Example: mydomain.com * Example: mydomain.com
* Port **(required)** * Port **(required)**
* The port to use when connecting to the server. * The port to use when connecting to the server.
* Example: 636 * Example: 636
* Enable TLS Encryption (optional) * Enable TLS Encryption (optional)
* Whether to use TLS when connecting to the LDAP server. * Whether to use TLS when connecting to the LDAP server.
* Admin Filter (optional) * Admin Filter (optional)
* An LDAP filter specifying if a user should be given administrator * An LDAP filter specifying if a user should be given administrator
privileges. If a user accounts passes the filter, the user will be privileges. If a user accounts passes the filter, the user will be
privileged as an administrator. privileged as an administrator.
* Example: (objectClass=adminAccount) * Example: (objectClass=adminAccount)
* First name attribute (optional) * First name attribute (optional)
* The attribute of the user's LDAP record containing the user's first name. * The attribute of the user's LDAP record containing the user's first name.
This will be used to populate their account information. This will be used to populate their account information.
* Example: givenName * Example: givenName
* Surname attribute (optional) * Surname attribute (optional)
* The attribute of the user's LDAP record containing the user's surname This * The attribute of the user's LDAP record containing the user's surname This
will be used to populate their account information. will be used to populate their account information.
* Example: sn * Example: sn
* E-mail attribute **(required)** * E-mail attribute **(required)**
* The attribute of the user's LDAP record containing the user's email * The attribute of the user's LDAP record containing the user's email
address. This will be used to populate their account information. address. This will be used to populate their account information.
* Example: mail * Example: mail
**LDAP via BindDN** adds the following fields: **LDAP via BindDN** adds the following fields:
* Bind DN (optional) * Bind DN (optional)
* The DN to bind to the LDAP server with when searching for the user. This * The DN to bind to the LDAP server with when searching for the user. This
may be left blank to perform an anonymous search. may be left blank to perform an anonymous search.
* Example: cn=Search,dc=mydomain,dc=com * Example: cn=Search,dc=mydomain,dc=com
* Bind Password (optional) * Bind Password (optional)
* The password for the Bind DN specified above, if any. _Note: The password * The password for the Bind DN specified above, if any. _Note: The password
is stored in plaintext at the server. As such, ensure that your Bind DN is stored in plaintext at the server. As such, ensure that your Bind DN
has as few privileges as possible._ has as few privileges as possible._
* User Search Base **(required)** * User Search Base **(required)**
* The LDAP base at which user accounts will be searched for. * The LDAP base at which user accounts will be searched for.
* Example: ou=Users,dc=mydomain,dc=com * Example: ou=Users,dc=mydomain,dc=com
* User Filter **(required)** * User Filter **(required)**
* An LDAP filter declaring how to find the user record that is attempting to * An LDAP filter declaring how to find the user record that is attempting to
authenticate. The '%s' matching parameter will be substituted with the authenticate. The '%s' matching parameter will be substituted with the
user's username. user's username.
* Example: (&(objectClass=posixAccount)(uid=%s)) * Example: (&(objectClass=posixAccount)(uid=%s))
**LDAP using simple auth** adds the following fields: **LDAP using simple auth** adds the following fields:
* User DN **(required)** * User DN **(required)**
* A template to use as the user's DN. The `%s` matching parameter will be * A template to use as the user's DN. The `%s` matching parameter will be
substituted with the user's username. substituted with the user's username.
* Example: cn=%s,ou=Users,dc=mydomain,dc=com * Example: cn=%s,ou=Users,dc=mydomain,dc=com
* Example: uid=%s,ou=Users,dc=mydomain,dc=com * Example: uid=%s,ou=Users,dc=mydomain,dc=com
* User Search Base (optional) * User Search Base (optional)
* The LDAP base at which user accounts will be searched for. * The LDAP base at which user accounts will be searched for.
* Example: ou=Users,dc=mydomain,dc=com * Example: ou=Users,dc=mydomain,dc=com
* User Filter **(required)** * User Filter **(required)**
* An LDAP filter declaring when a user should be allowed to log in. The `%s` * An LDAP filter declaring when a user should be allowed to log in. The `%s`
matching parameter will be substituted with the user's username. matching parameter will be substituted with the user's username.
* Example: (&(objectClass=posixAccount)(cn=%s)) * Example: (&(objectClass=posixAccount)(cn=%s))
* Example: (&(objectClass=posixAccount)(uid=%s)) * Example: (&(objectClass=posixAccount)(uid=%s))
**Verify group membership in LDAP** uses the following fields: **Verify group membership in LDAP** uses the following fields:
* Group Search Base (optional) * Group Search Base (optional)
* The LDAP DN used for groups. * The LDAP DN used for groups.
* Example: ou=group,dc=mydomain,dc=com * Example: ou=group,dc=mydomain,dc=com
* Group Name Filter (optional) * Group Name Filter (optional)
* An LDAP filter declaring how to find valid groups in the above DN. * An LDAP filter declaring how to find valid groups in the above DN.
* Example: (|(cn=gitea_users)(cn=admins)) * Example: (|(cn=gitea_users)(cn=admins))
* User Attribute in Group (optional) * User Attribute in Group (optional)
* Which user LDAP attribute is listed in the group. * Which user LDAP attribute is listed in the group.
* Example: uid * Example: uid
* Group Attribute for User (optional) * Group Attribute for User (optional)
* Which group LDAP attribute contains an array above user attribute names. * Which group LDAP attribute contains an array above user attribute names.
* Example: memberUid * Example: memberUid

View 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{}

View 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",
}

View 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{})
}

View 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
}

View file

@ -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

View 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
}

View 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
}
}

View 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{}

View 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
}

View file

@ -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 ""
}

View 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{})
}

View 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)
}

View 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
}

View 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
}

View 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())
}

View 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,
}

View 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{}

View 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{})
}

View 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)
}

View 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{}

View 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
}

View 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{})
}

View 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)
}

View 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{}

View 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{})
}

View file

@ -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
View 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
}

View file

@ -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>

View file

@ -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>

View file

@ -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}}