mirror of
https://github.com/go-gitea/gitea.git
synced 2025-05-05 15:32:53 +00:00
Fix #880 Design: 1. A global setting `security.TWO_FACTOR_AUTH`. * To support org-level config, we need to introduce a better "owner setting" system first (in the future) 2. A user without 2FA can login and may explore, but can NOT read or write to any repositories via API/web. 3. Keep things as simple as possible. * This option only aggressively suggest users to enable their 2FA at the moment, it does NOT guarantee that users must have 2FA before all other operations, it should be good enough for real world use cases. * Some details and tests could be improved in the future since this change only adds a check and seems won't affect too much. --------- Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
458 lines
12 KiB
Go
458 lines
12 KiB
Go
// 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/v2"
|
||
)
|
||
|
||
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)
|
||
}
|
||
)
|
||
|
||
var (
|
||
commonLdapCLIFlags = []cli.Flag{
|
||
&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.",
|
||
},
|
||
}
|
||
|
||
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.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",
|
||
})
|
||
|
||
ldapSimpleAuthCLIFlags = append(commonLdapCLIFlags,
|
||
&cli.StringFlag{
|
||
Name: "user-dn",
|
||
Usage: "The user's DN.",
|
||
})
|
||
|
||
microcmdAuthAddLdapBindDn = &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,
|
||
}
|
||
|
||
microcmdAuthUpdateLdapBindDn = &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...),
|
||
}
|
||
|
||
microcmdAuthAddLdapSimpleAuth = &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,
|
||
}
|
||
|
||
microcmdAuthUpdateLdapSimpleAuth = &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,
|
||
createAuthSource: auth.CreateSource,
|
||
updateAuthSource: auth.UpdateSource,
|
||
getAuthSourceByID: auth.GetSourceByID,
|
||
}
|
||
}
|
||
|
||
// parseAuthSourceLdap assigns values on authSource according to command line flags.
|
||
func parseAuthSourceLdap(c *cli.Context, 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.Context, 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.Context, authType auth.Type) (*auth.Source, error) {
|
||
if err := argsSet(c, "id"); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
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(c *cli.Context) error {
|
||
if err := argsSet(c, "name", "security-protocol", "host", "port", "user-search-base", "user-filter", "email-attribute"); err != nil {
|
||
return err
|
||
}
|
||
|
||
ctx, cancel := installSignals()
|
||
defer cancel()
|
||
|
||
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(c *cli.Context) error {
|
||
ctx, cancel := installSignals()
|
||
defer cancel()
|
||
|
||
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(c *cli.Context) error {
|
||
if err := argsSet(c, "name", "security-protocol", "host", "port", "user-dn", "user-filter", "email-attribute"); err != nil {
|
||
return err
|
||
}
|
||
|
||
ctx, cancel := installSignals()
|
||
defer cancel()
|
||
|
||
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(c *cli.Context) error {
|
||
ctx, cancel := installSignals()
|
||
defer cancel()
|
||
|
||
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)
|
||
}
|