diff --git a/examples/conf-from-labels.yml b/examples/conf-from-labels.yml index f42afb5..cadad37 100644 --- a/examples/conf-from-labels.yml +++ b/examples/conf-from-labels.yml @@ -38,7 +38,7 @@ services: - traefik.http.middlewares.ldap_auth.plugin.ldapAuth.port=389 - traefik.http.middlewares.ldap_auth.plugin.ldapAuth.baseDN=dc=example,dc=com - traefik.http.middlewares.ldap_auth.plugin.ldapAuth.attribute=uid - # AllowedGroups is not supported with labels, because multiple value labels are separated with commas + # AllowedGroups and AllowedUsers are not supported with labels, because multiple value labels are separated with commas # SearchFilter must not escape curly braces when using labels # - traefik.http.middlewares.ldap_auth.plugin.ldapAuth.searchFilter=({{.Attribute}}={{.Username}}) # ================================================================================================= diff --git a/examples/dynamic-conf/ldapAuth-conf.toml b/examples/dynamic-conf/ldapAuth-conf.toml index 13eef84..4cd8f87 100644 --- a/examples/dynamic-conf/ldapAuth-conf.toml +++ b/examples/dynamic-conf/ldapAuth-conf.toml @@ -7,6 +7,7 @@ LogLevel = "DEBUG" Port = "389" Url = "ldap://ldap.forumsys.com" AllowedGroups = ["ou=mathematicians,dc=example,dc=com","ou=italians,ou=scientists,dc=example,dc=com"] +AllowedUsers = ["euler", "euclid"] # SearchFilter must escape curly braces when using toml file # https://toml.io/en/v1.0.0#string # SearchFilter = '''(\{\{.Attribute\}\}=\{\{.Username\}\})''' diff --git a/examples/dynamic-conf/ldapAuth-conf.yml b/examples/dynamic-conf/ldapAuth-conf.yml index d8135f2..8895519 100644 --- a/examples/dynamic-conf/ldapAuth-conf.yml +++ b/examples/dynamic-conf/ldapAuth-conf.yml @@ -12,6 +12,9 @@ http: AllowedGroups: - ou=mathematicians,dc=example,dc=com - ou=italians,ou=scientists,dc=example,dc=com + AllowedUsers: + - euler + - euclid # SearchFilter must escape curly braces when using yml file # https://yaml.org/spec/1.1/#id872840 # SearchFilter: (\{\{.Attribute\}\}=\{\{.Username\}\}) diff --git a/ldapauth.go b/ldapauth.go index dbec586..2917fe6 100644 --- a/ldapauth.go +++ b/ldapauth.go @@ -60,6 +60,7 @@ type Config struct { WWWAuthenticateHeader bool `json:"wwwAuthenticateHeader,omitempty" yaml:"wwwAuthenticateHeader,omitempty"` WWWAuthenticateHeaderRealm string `json:"wwwAuthenticateHeaderRealm,omitempty" yaml:"wwwAuthenticateHeaderRealm,omitempty"` AllowedGroups []string `json:"allowedGroups,omitempty" yaml:"allowedGroups,omitempty"` + AllowedUsers []string `json:"allowedUsers,omitempty" yaml:"allowedUsers,omitempty"` Username string } @@ -89,6 +90,7 @@ func CreateConfig() *Config { WWWAuthenticateHeader: true, WWWAuthenticateHeaderRealm: "", AllowedGroups: nil, + AllowedUsers: nil, Username: "", } } @@ -134,6 +136,7 @@ func (la *LdapAuth) ServeHTTP(rw http.ResponseWriter, req *http.Request) { LoggerDEBUG.Printf("Session details: %v", session) username, password, ok := req.BasicAuth() + username = strings.ToLower(username) la.config.Username = username @@ -185,13 +188,12 @@ func (la *LdapAuth) ServeHTTP(rw http.ResponseWriter, req *http.Request) { return } - hasValidGroups, err := LdapCheckUserGroups(conn, la.config, entry, username) - - if !hasValidGroups { + isAuthorized, err := LdapCheckUserAuthorized(conn, la.config, entry, username) + if !isAuthorized { defer conn.Close() LoggerERROR.Printf("%s", err) RequireAuth(rw, req, la.config, err) - return + return } defer conn.Close() @@ -227,7 +229,7 @@ func (la *LdapAuth) ServeHTTP(rw http.ResponseWriter, req *http.Request) { la.next.ServeHTTP(rw, req) } -// LdapCheckUser chec if user and password are correct. +// LdapCheckUser check if user and password are correct. func LdapCheckUser(conn *ldap.Conn, config *Config, username, password string) (bool, *ldap.Entry, error) { if config.SearchFilter == "" { LoggerDEBUG.Printf("Running in Bind Mode") @@ -253,10 +255,60 @@ func LdapCheckUser(conn *ldap.Conn, config *Config, username, password string) ( return err == nil, result.Entries[0], err } +// LdapCheckUserAuthorized check if user is authorized post-authentication +func LdapCheckUserAuthorized(conn *ldap.Conn, config *Config, entry *ldap.Entry, username string) (bool, error) { + // Check if authorization is required or simply authentication + if len(config.AllowedUsers) == 0 && len(config.AllowedGroups) == 0 { + LoggerDEBUG.Printf("No authorization requirements") + return true, nil + } + + // Check if user is explicitly allowed + if LdapCheckAllowedUsers(conn, config, entry, username) { + return true, nil + } + + // Check if user is allowed through groups + isValidGroups, err := LdapCheckUserGroups(conn, config, entry, username) + if isValidGroups { + return true, err + } + + errMsg := fmt.Sprintf("User '%s' does not match any allowed users nor allowed groups.", username) + + if err != nil { + err = fmt.Errorf("%w\n%s", err, errMsg) + } else { + err = errors.New(errMsg) + } + + return false, err +} + +// LdapCheckAllowedUsers check if user is explicitly allowed in AllowedUsers list +func LdapCheckAllowedUsers(conn *ldap.Conn, config *Config, entry *ldap.Entry, username string) bool { + if len(config.AllowedUsers) == 0 { + return false + } + + found := false + + for _, u := range config.AllowedUsers { + lowerAllowedUser := strings.ToLower(u) + if lowerAllowedUser == username || lowerAllowedUser == strings.ToLower(entry.DN) { + LoggerDEBUG.Printf("User: '%s' explicitly allowed in AllowedUsers", entry.DN) + found = true + } + } + + return found +} + +// LdapCheckUserGroups check if the is user is a member of any of the AllowedGroups list func LdapCheckUserGroups(conn *ldap.Conn, config *Config, entry *ldap.Entry, username string) (bool, error) { if len(config.AllowedGroups) == 0 { - return true, nil + return false, nil } found := false @@ -300,7 +352,7 @@ func LdapCheckUserGroups(conn *ldap.Conn, config *Config, entry *ldap.Entry, use break } - err = fmt.Errorf("User not in any of the allowed groups") + LoggerDEBUG.Printf("User '%s' not in any of the allowed groups", username) } return found, err diff --git a/readme.md b/readme.md index 7444b68..e974b37 100644 --- a/readme.md +++ b/readme.md @@ -278,6 +278,17 @@ _Optional, Default: `[]`_ The list of LDAP group DNs that users must be members of to be granted access. If a user is in any of the listed groups, then that user is granted access. -If set to an empty list, all users with an LDAP account can log in, without performing any group membership checks. +If set to an empty list, all users with an LDAP account can log in, without performing any group membership checks unless `allowedUsers` is set. In that case, the user must be a part of the `allowedUsers` list. `allowedGroups` is not supported with labels, because multiple value labels are separated with commas. You must use `toml` or `yaml` configuration file. For more details, check [examples](https://github.com/wiltonsr/ldapAuth/tree/main/examples) page. + +##### `allowedUsers` +Needs `traefik` >= [`v2.8.2`](https://github.com/traefik/traefik/releases/tag/v2.8.2) + +_Optional, Default: `[]`_ + +The list of LDAP user DNs or usernames to be granted access. If a user is in the listed users, then that user is granted access. + +If set to an empty list, all users with an LDAP account can log in, unless `allowedGroups` is set. In that case, group membership checks will be performed. + +`allowedUsers` is not supported with labels, because multiple value labels are separated with commas. You must use `toml` or `yaml` configuration file. For more details, check [examples](https://github.com/wiltonsr/ldapAuth/tree/main/examples) page.