// Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT package cmd import ( "context" "fmt" "strings" "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/services/auth/source/ldap" "github.com/urfave/cli/v3" ) type ( authService struct { initDB func(ctx context.Context) error createAuthSource func(context.Context, *auth.Source) error updateAuthSource func(context.Context, *auth.Source) error getAuthSourceByID func(ctx context.Context, id int64) (*auth.Source, error) } ) func newMicrocmdAuthAddLdapBindDn() *cli.Command { return &cli.Command{ Name: "add-ldap", Usage: "Add new LDAP (via Bind DN) authentication source", Action: func(ctx context.Context, cmd *cli.Command) error { return newAuthService().addLdapBindDn(ctx, cmd) }, Flags: []cli.Flag{ &cli.StringFlag{Name: "name", Usage: "Authentication name.", Required: true}, &cli.BoolFlag{Name: "not-active", Usage: "Deactivate the authentication source."}, &cli.BoolFlag{Name: "active", Usage: "Activate the authentication source."}, &cli.StringFlag{Name: "security-protocol", Usage: "Security protocol name.", Required: true}, &cli.BoolFlag{Name: "skip-tls-verify", Usage: "Disable TLS verification."}, &cli.StringFlag{Name: "host", Usage: "The address where the LDAP server can be reached.", Required: true}, &cli.IntFlag{Name: "port", Usage: "The port to use when connecting to the LDAP server.", Required: true}, &cli.StringFlag{Name: "user-search-base", Usage: "The LDAP base at which user accounts will be searched for.", Required: true}, &cli.StringFlag{Name: "user-filter", Usage: "An LDAP filter declaring how to find the user record that is attempting to authenticate.", Required: true}, &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.", Required: true}, &cli.StringFlag{Name: "public-ssh-key-attribute", Usage: "The attribute of the user’s LDAP record containing the user’s public ssh key."}, &cli.BoolFlag{Name: "skip-local-2fa", Usage: "Set to true to skip local 2fa for users authenticated by this source"}, &cli.StringFlag{Name: "avatar-attribute", Usage: "The attribute of the user’s LDAP record containing the user’s avatar."}, &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.BoolFlag{Name: "disable-synchronize-users", Usage: "Disable user synchronization."}, &cli.UintFlag{Name: "page-size", Usage: "Search page size."}, &cli.BoolFlag{Name: "enable-groups", Usage: "Enable LDAP groups"}, &cli.StringFlag{Name: "group-search-base-dn", Usage: "The LDAP base DN at which group accounts will be searched for"}, &cli.StringFlag{Name: "group-member-attribute", Usage: "Group attribute containing list of users"}, &cli.StringFlag{Name: "group-user-attribute", Usage: "User attribute listed in group"}, &cli.StringFlag{Name: "group-filter", Usage: "Verify group membership in LDAP"}, &cli.StringFlag{Name: "group-team-map", Usage: "Map LDAP groups to Organization teams"}, &cli.BoolFlag{Name: "group-team-map-removal", Usage: "Remove users from synchronized teams if user does not belong to corresponding LDAP group"}, }, } } func newMicrocmdAuthUpdateLdapBindDn() *cli.Command { return &cli.Command{ Name: "update-ldap", Usage: "Update existing LDAP (via Bind DN) authentication source", Action: func(ctx context.Context, cmd *cli.Command) error { return newAuthService().updateLdapBindDn(ctx, cmd) }, Flags: []cli.Flag{ &cli.Int64Flag{Name: "id", Usage: "ID of authentication source", Required: true}, &cli.StringFlag{Name: "name", Usage: "Authentication name."}, &cli.BoolFlag{Name: "not-active", Usage: "Deactivate the authentication source."}, &cli.BoolFlag{Name: "active", Usage: "Activate 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."}, &cli.BoolFlag{Name: "skip-local-2fa", Usage: "Set to true to skip local 2fa for users authenticated by this source"}, &cli.StringFlag{Name: "avatar-attribute", Usage: "The attribute of the user’s LDAP record containing the user’s avatar."}, &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.BoolFlag{Name: "disable-synchronize-users", Usage: "Disable user synchronization."}, &cli.UintFlag{Name: "page-size", Usage: "Search page size."}, &cli.BoolFlag{Name: "enable-groups", Usage: "Enable LDAP groups"}, &cli.StringFlag{Name: "group-search-base-dn", Usage: "The LDAP base DN at which group accounts will be searched for"}, &cli.StringFlag{Name: "group-member-attribute", Usage: "Group attribute containing list of users"}, &cli.StringFlag{Name: "group-user-attribute", Usage: "User attribute listed in group"}, &cli.StringFlag{Name: "group-filter", Usage: "Verify group membership in LDAP"}, &cli.StringFlag{Name: "group-team-map", Usage: "Map LDAP groups to Organization teams"}, &cli.BoolFlag{Name: "group-team-map-removal", Usage: "Remove users from synchronized teams if user does not belong to corresponding LDAP group"}, }, } } func newMicrocmdAuthAddLdapSimpleAuth() *cli.Command { return &cli.Command{ Name: "add-ldap-simple", Usage: "Add new LDAP (simple auth) authentication source", Action: func(ctx context.Context, cmd *cli.Command) error { return newAuthService().addLdapSimpleAuth(ctx, cmd) }, Flags: []cli.Flag{ &cli.StringFlag{Name: "name", Usage: "Authentication name.", Required: true}, &cli.BoolFlag{Name: "not-active", Usage: "Deactivate the authentication source."}, &cli.BoolFlag{Name: "active", Usage: "Activate the authentication source."}, &cli.StringFlag{Name: "security-protocol", Usage: "Security protocol name.", Required: true}, &cli.BoolFlag{Name: "skip-tls-verify", Usage: "Disable TLS verification."}, &cli.StringFlag{Name: "host", Usage: "The address where the LDAP server can be reached.", Required: true}, &cli.IntFlag{Name: "port", Usage: "The port to use when connecting to the LDAP server.", Required: true}, &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.", Required: true}, &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.", Required: true}, &cli.StringFlag{Name: "public-ssh-key-attribute", Usage: "The attribute of the user’s LDAP record containing the user’s public ssh key."}, &cli.BoolFlag{Name: "skip-local-2fa", Usage: "Set to true to skip local 2fa for users authenticated by this source"}, &cli.StringFlag{Name: "avatar-attribute", Usage: "The attribute of the user’s LDAP record containing the user’s avatar."}, &cli.StringFlag{Name: "user-dn", Usage: "The user's DN.", Required: true}, }, } } func newMicrocmdAuthUpdateLdapSimpleAuth() *cli.Command { return &cli.Command{ Name: "update-ldap-simple", Usage: "Update existing LDAP (simple auth) authentication source", Action: func(ctx context.Context, cmd *cli.Command) error { return newAuthService().updateLdapSimpleAuth(ctx, cmd) }, Flags: []cli.Flag{ &cli.Int64Flag{Name: "id", Usage: "ID of authentication source", Required: true}, &cli.StringFlag{Name: "name", Usage: "Authentication name."}, &cli.BoolFlag{Name: "not-active", Usage: "Deactivate the authentication source."}, &cli.BoolFlag{Name: "active", Usage: "Activate 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."}, &cli.BoolFlag{Name: "skip-local-2fa", Usage: "Set to true to skip local 2fa for users authenticated by this source"}, &cli.StringFlag{Name: "avatar-attribute", Usage: "The attribute of the user’s LDAP record containing the user’s avatar."}, &cli.StringFlag{Name: "user-dn", Usage: "The user's DN."}, }, } } // newAuthService creates a service with default functions. func newAuthService() *authService { return &authService{ initDB: initDB, createAuthSource: auth.CreateSource, updateAuthSource: auth.UpdateSource, getAuthSourceByID: auth.GetSourceByID, } } // parseAuthSourceLdap assigns values on authSource according to command line flags. func parseAuthSourceLdap(c *cli.Command, authSource *auth.Source) { if c.IsSet("name") { authSource.Name = c.String("name") } if c.IsSet("not-active") { authSource.IsActive = !c.Bool("not-active") } if c.IsSet("active") { authSource.IsActive = c.Bool("active") } if c.IsSet("synchronize-users") { authSource.IsSyncEnabled = c.Bool("synchronize-users") } if c.IsSet("disable-synchronize-users") { authSource.IsSyncEnabled = !c.Bool("disable-synchronize-users") } authSource.TwoFactorPolicy = util.Iif(c.Bool("skip-local-2fa"), "skip", "") } // parseLdapConfig assigns values on config according to command line flags. func parseLdapConfig(c *cli.Command, config *ldap.Source) error { if c.IsSet("name") { config.Name = c.String("name") } if c.IsSet("host") { config.Host = c.String("host") } if c.IsSet("port") { config.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.SecurityProtocol = p } if c.IsSet("skip-tls-verify") { config.SkipVerify = c.Bool("skip-tls-verify") } if c.IsSet("bind-dn") { config.BindDN = c.String("bind-dn") } if c.IsSet("user-dn") { config.UserDN = c.String("user-dn") } if c.IsSet("bind-password") { config.BindPassword = c.String("bind-password") } if c.IsSet("user-search-base") { config.UserBase = c.String("user-search-base") } if c.IsSet("username-attribute") { config.AttributeUsername = c.String("username-attribute") } if c.IsSet("firstname-attribute") { config.AttributeName = c.String("firstname-attribute") } if c.IsSet("surname-attribute") { config.AttributeSurname = c.String("surname-attribute") } if c.IsSet("email-attribute") { config.AttributeMail = c.String("email-attribute") } if c.IsSet("attributes-in-bind") { config.AttributesInBind = c.Bool("attributes-in-bind") } if c.IsSet("public-ssh-key-attribute") { config.AttributeSSHPublicKey = c.String("public-ssh-key-attribute") } if c.IsSet("avatar-attribute") { config.AttributeAvatar = c.String("avatar-attribute") } if c.IsSet("page-size") { config.SearchPageSize = uint32(c.Uint("page-size")) } if c.IsSet("user-filter") { config.Filter = c.String("user-filter") } if c.IsSet("admin-filter") { config.AdminFilter = c.String("admin-filter") } if c.IsSet("restricted-filter") { config.RestrictedFilter = c.String("restricted-filter") } if c.IsSet("allow-deactivate-all") { config.AllowDeactivateAll = c.Bool("allow-deactivate-all") } if c.IsSet("enable-groups") { config.GroupsEnabled = c.Bool("enable-groups") } if c.IsSet("group-search-base-dn") { config.GroupDN = c.String("group-search-base-dn") } if c.IsSet("group-member-attribute") { config.GroupMemberUID = c.String("group-member-attribute") } if c.IsSet("group-user-attribute") { config.UserUID = c.String("group-user-attribute") } if c.IsSet("group-filter") { config.GroupFilter = c.String("group-filter") } if c.IsSet("group-team-map") { config.GroupTeamMap = c.String("group-team-map") } if c.IsSet("group-team-map-removal") { config.GroupTeamMapRemoval = c.Bool("group-team-map-removal") } 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 ldap.SecurityProtocolNames { if strings.EqualFold(name, n) { return i, true } } return 0, false } // getAuthSource 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) getAuthSource(ctx context.Context, c *cli.Command, authType auth.Type) (*auth.Source, error) { authSource, err := a.getAuthSourceByID(ctx, c.Int64("id")) if err != nil { return nil, err } if authSource.Type != authType { return nil, fmt.Errorf("Invalid authentication type. expected: %s, actual: %s", authType.String(), authSource.Type.String()) } return authSource, nil } // addLdapBindDn adds a new LDAP via Bind DN authentication source. func (a *authService) addLdapBindDn(ctx context.Context, c *cli.Command) error { if err := a.initDB(ctx); err != nil { return err } authSource := &auth.Source{ Type: auth.LDAP, IsActive: true, // active by default Cfg: &ldap.Source{ Enabled: true, // always true }, } parseAuthSourceLdap(c, authSource) if err := parseLdapConfig(c, authSource.Cfg.(*ldap.Source)); err != nil { return err } return a.createAuthSource(ctx, authSource) } // updateLdapBindDn updates a new LDAP via Bind DN authentication source. func (a *authService) updateLdapBindDn(ctx context.Context, c *cli.Command) error { if err := a.initDB(ctx); err != nil { return err } authSource, err := a.getAuthSource(ctx, c, auth.LDAP) if err != nil { return err } parseAuthSourceLdap(c, authSource) if err := parseLdapConfig(c, authSource.Cfg.(*ldap.Source)); err != nil { return err } return a.updateAuthSource(ctx, authSource) } // addLdapSimpleAuth adds a new LDAP (simple auth) authentication source. func (a *authService) addLdapSimpleAuth(ctx context.Context, c *cli.Command) error { if err := a.initDB(ctx); err != nil { return err } authSource := &auth.Source{ Type: auth.DLDAP, IsActive: true, // active by default Cfg: &ldap.Source{ Enabled: true, // always true }, } parseAuthSourceLdap(c, authSource) if err := parseLdapConfig(c, authSource.Cfg.(*ldap.Source)); err != nil { return err } return a.createAuthSource(ctx, authSource) } // updateLdapSimpleAuth updates a new LDAP (simple auth) authentication source. func (a *authService) updateLdapSimpleAuth(ctx context.Context, c *cli.Command) error { if err := a.initDB(ctx); err != nil { return err } authSource, err := a.getAuthSource(ctx, c, auth.DLDAP) if err != nil { return err } parseAuthSourceLdap(c, authSource) if err := parseLdapConfig(c, authSource.Cfg.(*ldap.Source)); err != nil { return err } return a.updateAuthSource(ctx, authSource) }