// Copyright 2019 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 cmd import ( "fmt" "strings" "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/auth/ldap" "github.com/urfave/cli" ) type ( authService struct { initDB func() error createLoginSource func(loginSource *models.LoginSource) error updateLoginSource func(loginSource *models.LoginSource) error getLoginSourceByID func(id int64) (*models.LoginSource, error) } ) var ( commonLdapCLIFlags = []cli.Flag{ cli.StringFlag{ Name: "name", Usage: "Authentication name.", }, cli.BoolFlag{ Name: "not-active", Usage: "Deactivate the authentication source.", }, cli.StringFlag{ Name: "security-protocol", Usage: "Security protocol name.", }, cli.BoolFlag{ Name: "skip-tls-verify", Usage: "Disable TLS verification.", }, cli.StringFlag{ Name: "host", Usage: "The address where the LDAP server can be reached.", }, cli.IntFlag{ Name: "port", Usage: "The port to use when connecting to the LDAP server.", }, cli.StringFlag{ Name: "user-search-base", Usage: "The LDAP base at which user accounts will be searched for.", }, cli.StringFlag{ Name: "user-filter", Usage: "An LDAP filter declaring how to find the user record that is attempting to authenticate.", }, cli.StringFlag{ Name: "admin-filter", Usage: "An LDAP filter specifying if a user should be given administrator privileges.", }, cli.StringFlag{ Name: "restricted-filter", Usage: "An LDAP filter specifying if a user should be given restricted status.", }, cli.BoolFlag{ Name: "allow-deactivate-all", Usage: "Allow empty search results to deactivate all users.", }, cli.StringFlag{ Name: "username-attribute", Usage: "The attribute of the user’s LDAP record containing the user name.", }, cli.StringFlag{ Name: "firstname-attribute", Usage: "The attribute of the user’s LDAP record containing the user’s first name.", }, cli.StringFlag{ Name: "surname-attribute", Usage: "The attribute of the user’s LDAP record containing the user’s surname.", }, cli.StringFlag{ Name: "email-attribute", Usage: "The attribute of the user’s LDAP record containing the user’s email address.", }, cli.StringFlag{ Name: "public-ssh-key-attribute", Usage: "The attribute of the user’s LDAP record containing the user’s public ssh key.", }, } ldapBindDnCLIFlags = append(commonLdapCLIFlags, cli.StringFlag{ Name: "bind-dn", Usage: "The DN to bind to the LDAP server with when searching for the user.", }, cli.StringFlag{ Name: "bind-password", Usage: "The password for the Bind DN, if any.", }, cli.BoolFlag{ Name: "attributes-in-bind", Usage: "Fetch attributes in bind DN context.", }, cli.BoolFlag{ Name: "synchronize-users", Usage: "Enable user synchronization.", }, cli.UintFlag{ Name: "page-size", Usage: "Search page size.", }) ldapSimpleAuthCLIFlags = append(commonLdapCLIFlags, cli.StringFlag{ Name: "user-dn", Usage: "The user’s DN.", }) cmdAuthAddLdapBindDn = cli.Command{ Name: "add-ldap", Usage: "Add new LDAP (via Bind DN) authentication source", Action: func(c *cli.Context) error { return newAuthService().addLdapBindDn(c) }, Flags: ldapBindDnCLIFlags, } cmdAuthUpdateLdapBindDn = cli.Command{ Name: "update-ldap", Usage: "Update existing LDAP (via Bind DN) authentication source", Action: func(c *cli.Context) error { return newAuthService().updateLdapBindDn(c) }, Flags: append([]cli.Flag{idFlag}, ldapBindDnCLIFlags...), } cmdAuthAddLdapSimpleAuth = cli.Command{ Name: "add-ldap-simple", Usage: "Add new LDAP (simple auth) authentication source", Action: func(c *cli.Context) error { return newAuthService().addLdapSimpleAuth(c) }, Flags: ldapSimpleAuthCLIFlags, } cmdAuthUpdateLdapSimpleAuth = cli.Command{ Name: "update-ldap-simple", Usage: "Update existing LDAP (simple auth) authentication source", Action: func(c *cli.Context) error { return newAuthService().updateLdapSimpleAuth(c) }, Flags: append([]cli.Flag{idFlag}, ldapSimpleAuthCLIFlags...), } ) // newAuthService creates a service with default functions. func newAuthService() *authService { return &authService{ initDB: initDB, createLoginSource: models.CreateLoginSource, updateLoginSource: models.UpdateSource, getLoginSourceByID: models.GetLoginSourceByID, } } // parseLoginSource assigns values on loginSource according to command line flags. func parseLoginSource(c *cli.Context, loginSource *models.LoginSource) { if c.IsSet("name") { loginSource.Name = c.String("name") } if c.IsSet("not-active") { loginSource.IsActived = !c.Bool("not-active") } if c.IsSet("synchronize-users") { loginSource.IsSyncEnabled = c.Bool("synchronize-users") } } // parseLdapConfig assigns values on config according to command line flags. func parseLdapConfig(c *cli.Context, config *models.LDAPConfig) error { if c.IsSet("name") { config.Source.Name = c.String("name") } if c.IsSet("host") { config.Source.Host = c.String("host") } if c.IsSet("port") { config.Source.Port = c.Int("port") } if c.IsSet("security-protocol") { p, ok := findLdapSecurityProtocolByName(c.String("security-protocol")) if !ok { return fmt.Errorf("Unknown security protocol name: %s", c.String("security-protocol")) } config.Source.SecurityProtocol = p } if c.IsSet("skip-tls-verify") { config.Source.SkipVerify = c.Bool("skip-tls-verify") } if c.IsSet("bind-dn") { config.Source.BindDN = c.String("bind-dn") } if c.IsSet("user-dn") { config.Source.UserDN = c.String("user-dn") } if c.IsSet("bind-password") { config.Source.BindPassword = c.String("bind-password") } if c.IsSet("user-search-base") { config.Source.UserBase = c.String("user-search-base") } if c.IsSet("username-attribute") { config.Source.AttributeUsername = c.String("username-attribute") } if c.IsSet("firstname-attribute") { config.Source.AttributeName = c.String("firstname-attribute") } if c.IsSet("surname-attribute") { config.Source.AttributeSurname = c.String("surname-attribute") } if c.IsSet("email-attribute") { config.Source.AttributeMail = c.String("email-attribute") } if c.IsSet("attributes-in-bind") { config.Source.AttributesInBind = c.Bool("attributes-in-bind") } if c.IsSet("public-ssh-key-attribute") { config.Source.AttributeSSHPublicKey = c.String("public-ssh-key-attribute") } if c.IsSet("page-size") { config.Source.SearchPageSize = uint32(c.Uint("page-size")) } if c.IsSet("user-filter") { config.Source.Filter = c.String("user-filter") } if c.IsSet("admin-filter") { config.Source.AdminFilter = c.String("admin-filter") } if c.IsSet("restricted-filter") { config.Source.RestrictedFilter = c.String("restricted-filter") } if c.IsSet("allow-deactivate-all") { config.Source.AllowDeactivateAll = c.Bool("allow-deactivate-all") } return nil } // findLdapSecurityProtocolByName finds security protocol by its name ignoring case. // It returns the value of the security protocol and if it was found. func findLdapSecurityProtocolByName(name string) (ldap.SecurityProtocol, bool) { for i, n := range models.SecurityProtocolNames { if strings.EqualFold(name, n) { return i, true } } return 0, false } // getLoginSource gets the login source by its id defined in the command line flags. // It returns an error if the id is not set, does not match any source or if the source is not of expected type. func (a *authService) getLoginSource(c *cli.Context, loginType models.LoginType) (*models.LoginSource, error) { if err := argsSet(c, "id"); err != nil { return nil, err } loginSource, err := a.getLoginSourceByID(c.Int64("id")) if err != nil { return nil, err } if loginSource.Type != loginType { return nil, fmt.Errorf("Invalid authentication type. expected: %s, actual: %s", models.LoginNames[loginType], models.LoginNames[loginSource.Type]) } return loginSource, nil } // addLdapBindDn adds a new LDAP via Bind DN authentication source. func (a *authService) addLdapBindDn(c *cli.Context) error { if err := argsSet(c, "name", "security-protocol", "host", "port", "user-search-base", "user-filter", "email-attribute"); err != nil { return err } if err := a.initDB(); err != nil { return err } loginSource := &models.LoginSource{ Type: models.LoginLDAP, IsActived: true, // active by default Cfg: &models.LDAPConfig{ Source: &ldap.Source{ Enabled: true, // always true }, }, } parseLoginSource(c, loginSource) if err := parseLdapConfig(c, loginSource.LDAP()); err != nil { return err } return a.createLoginSource(loginSource) } // updateLdapBindDn updates a new LDAP via Bind DN authentication source. func (a *authService) updateLdapBindDn(c *cli.Context) error { if err := a.initDB(); err != nil { return err } loginSource, err := a.getLoginSource(c, models.LoginLDAP) if err != nil { return err } parseLoginSource(c, loginSource) if err := parseLdapConfig(c, loginSource.LDAP()); err != nil { return err } return a.updateLoginSource(loginSource) } // addLdapSimpleAuth adds a new LDAP (simple auth) authentication source. func (a *authService) addLdapSimpleAuth(c *cli.Context) error { if err := argsSet(c, "name", "security-protocol", "host", "port", "user-dn", "user-filter", "email-attribute"); err != nil { return err } if err := a.initDB(); err != nil { return err } loginSource := &models.LoginSource{ Type: models.LoginDLDAP, IsActived: true, // active by default Cfg: &models.LDAPConfig{ Source: &ldap.Source{ Enabled: true, // always true }, }, } parseLoginSource(c, loginSource) if err := parseLdapConfig(c, loginSource.LDAP()); err != nil { return err } return a.createLoginSource(loginSource) } // updateLdapBindDn updates a new LDAP (simple auth) authentication source. func (a *authService) updateLdapSimpleAuth(c *cli.Context) error { if err := a.initDB(); err != nil { return err } loginSource, err := a.getLoginSource(c, models.LoginDLDAP) if err != nil { return err } parseLoginSource(c, loginSource) if err := parseLdapConfig(c, loginSource.LDAP()); err != nil { return err } return a.updateLoginSource(loginSource) }