Skip to content

Integ 2024 multidomain #3838

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
169 changes: 126 additions & 43 deletions connector/ldap/ldap.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ import (
type UserMatcher struct {
UserAttr string `json:"userAttr"`
GroupAttr string `json:"groupAttr"`
//Look for parent groups recursively
Recursive bool `json:"recursive"`
RecursionGroupAttr string `json:"recursionGroupAttr"`
}

// Config holds configuration options for LDAP logins.
Expand Down Expand Up @@ -142,6 +145,9 @@ type Config struct {
// TODO: should be eventually removed from the code
UserAttr string `json:"userAttr"`
GroupAttr string `json:"groupAttr"`
//Look for parent groups recursively
Recursive bool `json:"recursive"`
RecursionGroupAttr string `json:"recursionGroupAttr"`

// Array of the field pairs used to match a user to a group.
// See the "UserMatcher" struct for the exact field names
Expand All @@ -159,6 +165,10 @@ type Config struct {
} `json:"groupSearch"`
}

const (
SamAccountNameAttr = "sAMAccountName"
)

func scopeString(i int) string {
switch i {
case ldap.ScopeBaseObject:
Expand Down Expand Up @@ -196,8 +206,10 @@ func userMatchers(c *Config, logger log.Logger) []UserMatcher {
log.Deprecated(logger, `LDAP: use groupSearch.userMatchers option instead of "userAttr/groupAttr" fields.`)
return []UserMatcher{
{
UserAttr: c.GroupSearch.UserAttr,
GroupAttr: c.GroupSearch.GroupAttr,
UserAttr: c.GroupSearch.UserAttr,
GroupAttr: c.GroupSearch.GroupAttr,
Recursive: c.GroupSearch.Recursive,
RecursionGroupAttr: c.GroupSearch.RecursionGroupAttr,
},
}
}
Expand Down Expand Up @@ -391,12 +403,23 @@ func (c *ldapConnector) identityFromEntry(user ldap.Entry) (ident connector.Iden
missing = append(missing, c.UserSearch.PreferredUsernameAttrAttr)
}
}

var lSamName string
if c.UserSearch.EmailSuffix != "" {
ident.Email = ident.Username + "@" + c.UserSearch.EmailSuffix
} else if ident.Email = c.getAttr(user, c.UserSearch.EmailAttr); ident.Email == "" {
missing = append(missing, c.UserSearch.EmailAttr)
} else {
ident.Email = c.getAttr(user, c.UserSearch.EmailAttr)
lSamName = c.getAttr(user, SamAccountNameAttr)

// If username search is mailOrSAMAccountName, skip email validation if sAMAccountName is present
if c.UserSearch.Username == "mailOrSAMAccountName" {
if ident.Email == "" && lSamName == "" {
missing = append(missing, c.UserSearch.EmailAttr)
}
} else if ident.Email == "" {
missing = append(missing, c.UserSearch.EmailAttr)
}
}

// TODO(ericchiang): Let this value be set from an attribute.
ident.EmailVerified = true

Expand All @@ -412,6 +435,9 @@ func (c *ldapConnector) userEntry(conn *ldap.Conn, username string) (user ldap.E
if c.UserSearch.Filter != "" {
filter = fmt.Sprintf("(&%s%s)", c.UserSearch.Filter, filter)
}
if c.UserSearch.Username == "mailOrSAMAccountName" {
filter = fmt.Sprintf("(|(mail=%s)(sAMAccountName=%s))", ldap.EscapeFilter(username), ldap.EscapeFilter(username))
}

// Initial search.
req := &ldap.SearchRequest{
Expand All @@ -422,6 +448,7 @@ func (c *ldapConnector) userEntry(conn *ldap.Conn, username string) (user ldap.E
Attributes: []string{
c.UserSearch.IDAttr,
c.UserSearch.EmailAttr,
SamAccountNameAttr,
// TODO(ericchiang): what if this contains duplicate values?
},
}
Expand All @@ -448,7 +475,8 @@ func (c *ldapConnector) userEntry(conn *ldap.Conn, username string) (user ldap.E
switch n := len(resp.Entries); n {
case 0:
c.logger.Errorf("ldap: no results returned for filter: %q", filter)
return ldap.Entry{}, false, nil
return ldap.Entry{}, false, fmt.Errorf("ldap: no results returned for filter: %q LDAP Result Code %d %q: ", filter,
ldap.LDAPResultInvalidCredentials, ldap.LDAPResultCodeMap[ldap.LDAPResultInvalidCredentials])
case 1:
user = *resp.Entries[0]
c.logger.Infof("username %q mapped to entry %s", username, user.DN)
Expand Down Expand Up @@ -493,7 +521,8 @@ func (c *ldapConnector) Login(ctx context.Context, s connector.Scopes, username,
case ldap.LDAPResultInvalidCredentials:
c.logger.Errorf("ldap: invalid password for user %q", user.DN)
incorrectPass = true
return nil
return fmt.Errorf("invalid credentials for user %q LDAP Result Code %d %q: ", user.DN,
ldap.LDAPResultInvalidCredentials, ldap.LDAPResultCodeMap[ldap.LDAPResultInvalidCredentials])
case ldap.LDAPResultConstraintViolation:
c.logger.Errorf("ldap: constraint violation for user %q: %s", user.DN, ldapErr.Error())
incorrectPass = true
Expand Down Expand Up @@ -586,56 +615,110 @@ func (c *ldapConnector) groups(ctx context.Context, user ldap.Entry) ([]string,
}

var groups []*ldap.Entry
var groupNames []string
for _, matcher := range c.GroupSearch.UserMatchers {
for _, attr := range c.getAttrs(user, matcher.UserAttr) {
filter := fmt.Sprintf("(%s=%s)", matcher.GroupAttr, ldap.EscapeFilter(attr))
if c.GroupSearch.Filter != "" {
filter = fmt.Sprintf("(&%s%s)", c.GroupSearch.Filter, filter)
if obtainedGroups, filter, err := c.queryGroups(ctx, matcher.GroupAttr, attr); err == nil {
groups = append(groups, obtainedGroups...)
if len(obtainedGroups) == 0 {
// TODO(ericchiang): Is this going to spam the logs?
c.logger.Errorf("ldap: groups search with filter %q returned no groups", filter)
}
} else {
return nil, err
}
}
}

req := &ldap.SearchRequest{
BaseDN: c.GroupSearch.BaseDN,
Filter: filter,
Scope: c.groupSearchScope,
Attributes: []string{c.GroupSearch.NameAttr},
for {
// Temporal variable used to reset variable groups on each cycle
var nextLevelGroups []*ldap.Entry
for _, group := range groups {
name := c.getAttr(*group, c.GroupSearch.NameAttr)
if name == "" {
// Be obnoxious about missing missing attributes. If the group entry is
// missing its name attribute, that indicates a misconfiguration.
//
// In the future we can add configuration options to just log these errors.
return nil, fmt.Errorf("ldap: group entity %q missing required attribute %q",
group.DN, c.GroupSearch.NameAttr)
}

gotGroups := false
if err := c.do(ctx, func(conn *ldap.Conn) error {
c.logger.Infof("performing ldap search %s %s %s",
req.BaseDN, scopeString(req.Scope), req.Filter)
resp, err := conn.Search(req)
if err != nil {
return fmt.Errorf("ldap: search failed: %v", err)
// Prevent duplicates and circular hierarchy therefore infinite loops
exit := false
for _, groupName := range groupNames {
if name == groupName {
c.logger.Infof("Found duplicate group with name %s", name)
exit = true
break
}
gotGroups = len(resp.Entries) != 0
groups = append(groups, resp.Entries...)
return nil
}); err != nil {
return nil, err
c.logger.Infof("Comparing %s with %s", name, groupName)
}
if !gotGroups {
// TODO(ericchiang): Is this going to spam the logs?
c.logger.Errorf("ldap: groups search with filter %q returned no groups", filter)
if exit {
continue
}

groupNames = append(groupNames, name)
for _, matcher := range c.GroupSearch.UserMatchers {
if matcher.Recursive {
c.logger.Infof("Recursive search enabled for groups")
if matcher.RecursionGroupAttr == "" {
return nil, fmt.Errorf("ldap: recursionGroupAttr attribute is not set but recursive search is enabled")
}
if obtainedGroups, _, err := c.queryGroups(ctx, matcher.RecursionGroupAttr, group.DN); err == nil {
// Keep searching upwards
if len(obtainedGroups) != 0 {
nextLevelGroups = append(nextLevelGroups, obtainedGroups...)
} else {
c.logger.Infof("Didn't find parents for group with name %s", name)
}
} else {
return nil, err
}
}
}
}
// If there's no remaining levels -> exit loop
// No duplicated group would reach this code
if len(nextLevelGroups) == 0 {
break
}
// reassign groups for next iteration
groups = nextLevelGroups
}
return groupNames, nil
}

groupNames := make([]string, 0, len(groups))
for _, group := range groups {
name := c.getAttr(*group, c.GroupSearch.NameAttr)
if name == "" {
// Be obnoxious about missing attributes. If the group entry is
// missing its name attribute, that indicates a misconfiguration.
//
// In the future we can add configuration options to just log these errors.
return nil, fmt.Errorf("ldap: group entity %q missing required attribute %q",
group.DN, c.GroupSearch.NameAttr)
// Query groups for users and groups
func (c *ldapConnector) queryGroups(ctx context.Context, memberAttr string, dn string) ([]*ldap.Entry, string, error) {
var groups []*ldap.Entry
filter := fmt.Sprintf("(%s=%s)", memberAttr, ldap.EscapeFilter(dn))
if c.GroupSearch.Filter != "" {
filter = fmt.Sprintf("(&%s%s)", c.GroupSearch.Filter, filter)
}
req := &ldap.SearchRequest{
BaseDN: c.GroupSearch.BaseDN,
Filter: filter,
Scope: c.groupSearchScope,
Attributes: []string{c.GroupSearch.NameAttr},
}
if err := c.do(ctx, func(conn *ldap.Conn) error {
c.logger.Infof("performing ldap search %s %s %s",
req.BaseDN, scopeString(req.Scope), req.Filter)
resp, err := conn.Search(req)
if err != nil {
if ldapErr, ok := err.(*ldap.Error); ok && ldapErr.ResultCode == ldap.LDAPResultNoSuchObject {
c.logger.Infof("ldap: groups search with filter %q returned no groups", filter)
return nil
}
return fmt.Errorf("ldap: search failed: %v", err)
}

groupNames = append(groupNames, name)
groups = append(groups, resp.Entries...)
return nil
}); err != nil {
return nil, filter, err
}
return groupNames, nil
return groups, filter, nil
}

func (c *ldapConnector) Prompt() string {
Expand Down
29 changes: 24 additions & 5 deletions server/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"encoding/base64"
"encoding/json"
"fmt"
"github.com/go-jose/go-jose/v4"
"html/template"
"net/http"
"net/url"
Expand All @@ -18,7 +19,6 @@ import (
"time"

"github.com/coreos/go-oidc/v3/oidc"
"github.com/go-jose/go-jose/v4"
"github.com/gorilla/mux"

"github.com/dexidp/dex/connector"
Expand Down Expand Up @@ -368,7 +368,7 @@ func (s *Server) handlePasswordLogin(w http.ResponseWriter, r *http.Request) {
identity, ok, err := pwConn.Login(ctx, scopes, username, password)
if err != nil {
s.logger.Errorf("Failed to login user: %v", err)
s.renderError(r, w, http.StatusInternalServerError, fmt.Sprintf("Login error: %v", err))
s.renderError(r, w, http.StatusInternalServerError, "Login Error.", err.Error())
return
}
if !ok {
Expand Down Expand Up @@ -1464,10 +1464,29 @@ func (s *Server) writeAccessToken(w http.ResponseWriter, resp *accessTokenRespon
w.Write(data)
}

func (s *Server) renderError(r *http.Request, w http.ResponseWriter, status int, description string) {
if err := s.templates.err(r, w, status, description); err != nil {
s.logger.Errorf("server template error: %v", err)
func (s *Server) renderError(r *http.Request, w http.ResponseWriter, status int, description string, errors ...string) {
if r == nil {
s.logger.Error("Cannot render error. Request not found.")
return
}
// Write json based errors instead of using the template to render error.
resp := struct {
ErrorMessage string `json:"errorMsg"`
ErrorDetails string `json:"errorDetails"`
}{
description,
strings.Join(errors, ", "),
}
data, err := json.Marshal(resp)
if err != nil {
s.logger.Errorf("failed to render error: %v", err)
s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Content-Length", strconv.Itoa(len(data)))
w.WriteHeader(status)
w.Write(data)
}

func (s *Server) tokenErrHelper(w http.ResponseWriter, typ string, description string, statusCode int) {
Expand Down