diff --git a/ldap/server.go b/ldap/server.go index cf1aa4ff..182f94e0 100644 --- a/ldap/server.go +++ b/ldap/server.go @@ -18,6 +18,7 @@ import ( "fmt" "hash/fnv" "log" + "strings" "github.com/casdoor/casdoor/conf" "github.com/casdoor/casdoor/object" @@ -49,7 +50,17 @@ func handleBind(w ldap.ResponseWriter, m *ldap.Message) { res := ldap.NewBindResponse(ldap.LDAPResultSuccess) if r.AuthenticationChoice() == "simple" { - bindUsername, bindOrg, err := getNameAndOrgFromDN(string(r.Name())) + bindDN := string(r.Name()) + bindPassword := string(r.AuthenticationSimple()) + + if bindDN == "" && bindPassword == "" { + res.SetResultCode(ldap.LDAPResultInappropriateAuthentication) + res.SetDiagnosticMessage("Anonymous bind disallowed") + w.Write(res) + return + } + + bindUsername, bindOrg, err := getNameAndOrgFromDN(bindDN) if err != nil { log.Printf("getNameAndOrgFromDN() error: %s", err.Error()) res.SetResultCode(ldap.LDAPResultInvalidDNSyntax) @@ -58,7 +69,6 @@ func handleBind(w ldap.ResponseWriter, m *ldap.Message) { return } - bindPassword := string(r.AuthenticationSimple()) bindUser, err := object.CheckUserPassword(bindOrg, bindUsername, bindPassword, "en") if err != nil { log.Printf("Bind failed User=%s, Pass=%#v, ErrMsg=%s", string(r.Name()), r.Authentication(), err) @@ -93,7 +103,46 @@ func handleSearch(w ldap.ResponseWriter, m *ldap.Message) { } r := m.GetSearchRequest() - if r.FilterString() == "(objectClass=*)" { + + // case insensitive match + if strings.EqualFold(r.FilterString(), "(objectClass=*)") { + if len(r.Attributes()) == 0 { + w.Write(res) + return + } + first_attr := string(r.Attributes()[0]) + + if string(r.BaseObject()) == "" { + // handle special search requests + + if first_attr == "namingContexts" { + orgs, code := GetFilteredOrganizations(m) + if code != ldap.LDAPResultSuccess { + res.SetResultCode(code) + w.Write(res) + return + } + e := ldap.NewSearchResultEntry(string(r.BaseObject())) + dnlist := make([]message.AttributeValue, len(orgs)) + for i, org := range orgs { + dnlist[i] = message.AttributeValue(fmt.Sprintf("ou=%s", org.Name)) + } + e.AddAttribute("namingContexts", dnlist...) + w.Write(e) + } else if first_attr == "subschemaSubentry" { + e := ldap.NewSearchResultEntry(string(r.BaseObject())) + e.AddAttribute("subschemaSubentry", message.AttributeValue("cn=Subschema")) + w.Write(e) + } + } else if strings.EqualFold(first_attr, "objectclasses") && string(r.BaseObject()) == "cn=Subschema" { + e := ldap.NewSearchResultEntry(string(r.BaseObject())) + e.AddAttribute("objectClasses", []message.AttributeValue{ + "( 1.3.6.1.1.1.2.0 NAME 'posixAccount' DESC 'Abstraction of an account with POSIX attributes' SUP top AUXILIARY MUST ( cn $ uid $ uidNumber $ gidNumber $ homeDirectory ) MAY ( userPassword $ loginShell $ gecos $ description ) )", + "( 1.3.6.1.1.1.2.2 NAME 'posixGroup' DESC 'Abstraction of a group of accounts' SUP top STRUCTURAL MUST ( cn $ gidNumber ) MAY ( userPassword $ memberUid $ description ) )", + }...) + w.Write(e) + } + w.Write(res) return } @@ -106,38 +155,72 @@ func handleSearch(w ldap.ResponseWriter, m *ldap.Message) { default: } - users, code := GetFilteredUsers(m) - if code != ldap.LDAPResultSuccess { - res.SetResultCode(code) - w.Write(res) - return - } - - for _, user := range users { - dn := fmt.Sprintf("uid=%s,cn=%s,%s", user.Id, user.Name, string(r.BaseObject())) - e := ldap.NewSearchResultEntry(dn) - uidNumberStr := fmt.Sprintf("%v", hash(user.Name)) - e.AddAttribute("uidNumber", message.AttributeValue(uidNumberStr)) - e.AddAttribute("gidNumber", message.AttributeValue(uidNumberStr)) - e.AddAttribute("homeDirectory", message.AttributeValue("/home/"+user.Name)) - e.AddAttribute("cn", message.AttributeValue(user.Name)) - e.AddAttribute("uid", message.AttributeValue(user.Id)) - attrs := r.Attributes() - for _, attr := range attrs { - if string(attr) == "*" { - attrs = AdditionalLdapAttributes - break - } - } - for _, attr := range attrs { - e.AddAttribute(message.AttributeDescription(attr), getAttribute(string(attr), user)) - if string(attr) == "cn" { - e.AddAttribute(message.AttributeDescription(attr), getAttribute("title", user)) - } + objectClass := searchFilterForEquality(r.Filter(), "objectClass", "posixAccount", "posixGroup") + switch objectClass { + case "posixAccount": + users, code := GetFilteredUsers(m) + if code != ldap.LDAPResultSuccess { + res.SetResultCode(code) + w.Write(res) + return } - w.Write(e) + // log.Printf("Handling posixAccount filter=%s", r.FilterString()) + for _, user := range users { + dn := fmt.Sprintf("uid=%s,cn=users,%s", user.Name, string(r.BaseObject())) + e := ldap.NewSearchResultEntry(dn) + attrs := r.Attributes() + for _, attr := range attrs { + if string(attr) == "*" { + attrs = AdditionalLdapUserAttributes + break + } + } + for _, attr := range attrs { + if strings.HasSuffix(string(attr), ";binary") { + // unsupported: userCertificate;binary + continue + } + field, ok := ldapUserAttributesMapping.CaseInsensitiveGet(string(attr)) + if ok { + e.AddAttribute(message.AttributeDescription(attr), field.GetAttributeValues(user)...) + } + } + w.Write(e) + } + + case "posixGroup": + // log.Printf("Handling posixGroup filter=%s", r.FilterString()) + groups, code := GetFilteredGroups(m) + if code != ldap.LDAPResultSuccess { + res.SetResultCode(code) + w.Write(res) + return + } + + for _, group := range groups { + dn := fmt.Sprintf("cn=%s,cn=groups,%s", group.Name, string(r.BaseObject())) + e := ldap.NewSearchResultEntry(dn) + attrs := r.Attributes() + for _, attr := range attrs { + if string(attr) == "*" { + attrs = AdditionalLdapGroupAttributes + break + } + } + for _, attr := range attrs { + field, ok := ldapGroupAttributesMapping.CaseInsensitiveGet(string(attr)) + if ok { + e.AddAttribute(message.AttributeDescription(attr), field.GetAttributeValues(group)...) + } + } + w.Write(e) + } + + case "": + log.Printf("Unmatched search request. filter=%s", r.FilterString()) } + w.Write(res) } diff --git a/ldap/util.go b/ldap/util.go index 81db479c..35485728 100644 --- a/ldap/util.go +++ b/ldap/util.go @@ -18,6 +18,7 @@ import ( "fmt" "log" "strings" + "time" "github.com/casdoor/casdoor/object" "github.com/casdoor/casdoor/util" @@ -28,65 +29,259 @@ import ( "github.com/xorm-io/builder" ) -type AttributeMapper func(user *object.User) message.AttributeValue +type V = message.AttributeValue -type FieldRelation struct { +type UserAttributeMapper func(user *object.User) []V + +type UserFieldRelation struct { userField string + ldapField string notSearchable bool hideOnStarOp bool - fieldMapper AttributeMapper + fieldMapper UserAttributeMapper + constantValue []V } -func (rel FieldRelation) GetField() (string, error) { +func (rel UserFieldRelation) GetField() (string, error) { if rel.notSearchable { return "", fmt.Errorf("attribute %s not supported", rel.userField) } return rel.userField, nil } -func (rel FieldRelation) GetAttributeValue(user *object.User) message.AttributeValue { +func (rel UserFieldRelation) GetAttributeValues(user *object.User) []V { + if rel.constantValue != nil && rel.fieldMapper == nil { + return rel.constantValue + } return rel.fieldMapper(user) } -var ldapAttributesMapping = map[string]FieldRelation{ - "cn": {userField: "name", hideOnStarOp: true, fieldMapper: func(user *object.User) message.AttributeValue { - return message.AttributeValue(user.Name) - }}, - "uid": {userField: "name", hideOnStarOp: true, fieldMapper: func(user *object.User) message.AttributeValue { - return message.AttributeValue(user.Name) - }}, - "displayname": {userField: "displayName", fieldMapper: func(user *object.User) message.AttributeValue { - return message.AttributeValue(user.DisplayName) - }}, - "email": {userField: "email", fieldMapper: func(user *object.User) message.AttributeValue { - return message.AttributeValue(user.Email) - }}, - "mail": {userField: "email", fieldMapper: func(user *object.User) message.AttributeValue { - return message.AttributeValue(user.Email) - }}, - "mobile": {userField: "phone", fieldMapper: func(user *object.User) message.AttributeValue { - return message.AttributeValue(user.Phone) - }}, - "title": {userField: "tag", fieldMapper: func(user *object.User) message.AttributeValue { - return message.AttributeValue(user.Tag) - }}, - "userPassword": { - userField: "userPassword", - notSearchable: true, - fieldMapper: func(user *object.User) message.AttributeValue { - return message.AttributeValue(getUserPasswordWithType(user)) - }, - }, +type UserFieldRelationMap map[string]UserFieldRelation + +func (m UserFieldRelationMap) CaseInsensitiveGet(key string) (UserFieldRelation, bool) { + lowerKey := strings.ToLower(key) + ret, ok := m[lowerKey] + return ret, ok } -var AdditionalLdapAttributes []message.LDAPString +type GroupAttributeMapper func(group *object.Group) []V + +type GroupFieldRelation struct { + groupField string + ldapField string + notSearchable bool + hideOnStarOp bool + fieldMapper GroupAttributeMapper + constantValue []V +} + +func (rel GroupFieldRelation) GetField() (string, error) { + if rel.notSearchable { + return "", fmt.Errorf("attribute %s not supported", rel.groupField) + } + return rel.groupField, nil +} + +func (rel GroupFieldRelation) GetAttributeValues(group *object.Group) []V { + if rel.constantValue != nil && rel.fieldMapper == nil { + return rel.constantValue + } + return rel.fieldMapper(group) +} + +type GroupFieldRelationMap map[string]GroupFieldRelation + +func (m GroupFieldRelationMap) CaseInsensitiveGet(key string) (GroupFieldRelation, bool) { + lowerKey := strings.ToLower(key) + ret, ok := m[lowerKey] + return ret, ok +} + +var ldapUserAttributesMapping = UserFieldRelationMap{ + "cn": {ldapField: "cn", userField: "name", hideOnStarOp: true, fieldMapper: func(user *object.User) []V { + return []V{V(user.Name)} + }}, + "uid": {ldapField: "uid", userField: "name", hideOnStarOp: true, fieldMapper: func(user *object.User) []V { + return []V{V(user.Name)} + }}, + "displayname": {ldapField: "displayName", userField: "displayName", fieldMapper: func(user *object.User) []V { + return []V{V(user.DisplayName)} + }}, + "email": {ldapField: "email", userField: "email", fieldMapper: func(user *object.User) []V { + return []V{V(user.Email)} + }}, + "mail": {ldapField: "mail", userField: "email", fieldMapper: func(user *object.User) []V { + return []V{V(user.Email)} + }}, + "mobile": {ldapField: "mobile", userField: "phone", fieldMapper: func(user *object.User) []V { + return []V{V(user.Phone)} + }}, + "telephonenumber": {ldapField: "telephoneNumber", userField: "phone", fieldMapper: func(user *object.User) []V { + return []V{V(user.Phone)} + }}, + "postaladdress": {ldapField: "postalAddress", userField: "address", fieldMapper: func(user *object.User) []V { + return []V{V(strings.Join(user.Address, " "))} + }}, + "title": {ldapField: "title", userField: "title", fieldMapper: func(user *object.User) []V { + return []V{V(user.Title)} + }}, + "gecos": {ldapField: "gecos", userField: "displayName", fieldMapper: func(user *object.User) []V { + return []V{V(user.DisplayName)} + }}, + "description": {ldapField: "description", userField: "displayName", fieldMapper: func(user *object.User) []V { + return []V{V(user.DisplayName)} + }}, + "logindisabled": {ldapField: "loginDisabled", userField: "isForbidden", fieldMapper: func(user *object.User) []V { + if user.IsForbidden { + return []V{V("1")} + } else { + return []V{V("0")} + } + }}, + "userpassword": { + ldapField: "userPassword", + userField: "userPassword", + notSearchable: true, + fieldMapper: func(user *object.User) []V { + return []V{V(getUserPasswordWithType(user))} + }, + }, + "uidnumber": {ldapField: "uidNumber", notSearchable: true, fieldMapper: func(user *object.User) []V { + return []V{V(fmt.Sprintf("%v", hash(user.Name)))} + }}, + "gidnumber": {ldapField: "gidNumber", notSearchable: true, fieldMapper: func(user *object.User) []V { + if len(user.Groups) == 0 { + return []V{V("")} + } + group, err := object.GetGroup(user.Groups[0]) + if err != nil { + log.Printf("gidnumber object.GetGroup error: %s", err) + return []V{V("")} + } + return []V{V(fmt.Sprintf("%v", hash(group.Name)))} + }}, + "homedirectory": {ldapField: "homeDirectory", notSearchable: true, fieldMapper: func(user *object.User) []V { + return []V{V("/home/" + user.Name)} + }}, + "loginshell": {ldapField: "loginShell", notSearchable: true, fieldMapper: func(user *object.User) []V { + if user.IsForbidden || user.IsDeleted { + return []V{V("/sbin/nologin")} + } else { + return []V{V("/bin/bash")} + } + }}, + "shadowlastchange": {ldapField: "shadowLastChange", notSearchable: true, fieldMapper: func(user *object.User) []V { + // "this attribute specifies number of days between January 1, 1970, and the date that the password was last modified" + updatedTime, err := time.Parse(time.RFC3339, user.UpdatedTime) + if err != nil { + log.Printf("shadowlastchange time.Parse error: %s", err) + updatedTime = time.Now() + } + return []V{V(fmt.Sprint(updatedTime.Unix() / 86400))} + }}, + "pwdchangedtime": {ldapField: "pwdChangedTime", notSearchable: true, fieldMapper: func(user *object.User) []V { + updatedTime, err := time.Parse(time.RFC3339, user.UpdatedTime) + if err != nil { + log.Printf("pwdchangedtime time.Parse error: %s", err) + updatedTime = time.Now() + } + return []V{V(updatedTime.UTC().Format("20060102030405Z"))} + }}, + "shadowmin": {ldapField: "shadowMin", notSearchable: true, constantValue: []V{V("0")}}, + "shadowmax": {ldapField: "shadowMax", notSearchable: true, constantValue: []V{V("99999")}}, + "shadowwarning": {ldapField: "shadowWarning", notSearchable: true, constantValue: []V{V("7")}}, + "shadowexpire": {ldapField: "shadowExpire", notSearchable: true, fieldMapper: func(user *object.User) []V { + if user.IsForbidden { + return []V{V("1")} + } else { + return []V{V("-1")} + } + }}, + "shadowinactive": {ldapField: "shadowInactive", notSearchable: true, constantValue: []V{V("0")}}, + "shadowflag": {ldapField: "shadowFlag", notSearchable: true, constantValue: []V{V("0")}}, + "memberof": {ldapField: "memberOf", notSearchable: true, fieldMapper: func(user *object.User) []V { + var groupdn []V + for _, groupId := range user.Groups { + group, err := object.GetGroup(groupId) + if err != nil { + log.Printf("memberOf object.GetGroup error: %s", err) + continue + } + groupdn = append(groupdn, V(fmt.Sprintf("cn=%s,cn=groups,ou=%s", group.Name, group.Owner))) + } + return groupdn + }}, + "objectclass": {ldapField: "objectClass", notSearchable: true, constantValue: []V{ + V("top"), + V("posixAccount"), + V("shadowAccount"), + V("person"), + V("organizationalPerson"), + V("inetOrgPerson"), + V("apple-user"), + V("sambaSamAccount"), + V("sambaIdmapEntry"), + V("extensibleObject"), + }}, +} + +var ldapGroupAttributesMapping = GroupFieldRelationMap{ + "cn": {ldapField: "cn", hideOnStarOp: true, fieldMapper: func(group *object.Group) []V { + return []V{V(group.Name)} + }}, + "gidnumber": {ldapField: "gidNumber", hideOnStarOp: true, fieldMapper: func(group *object.Group) []V { + return []V{V(fmt.Sprintf("%v", hash(group.Name)))} + }}, + "member": {ldapField: "member", fieldMapper: func(group *object.Group) []V { + users, err := object.GetGroupUsers(group.GetId()) + if err != nil { + log.Printf("member object.GetGroupUsers error: %s", err) + return []V{V("")} + } + var members []V + for _, user := range users { + members = append(members, V(fmt.Sprintf("uid=%s,cn=users,ou=%s", user.Name, user.Owner))) + } + return members + }}, + "memberuid": {ldapField: "memberUid", fieldMapper: func(group *object.Group) []V { + users, err := object.GetGroupUsers(group.GetId()) + if err != nil { + log.Printf("member object.GetGroupUsers error: %s", err) + return []V{V("")} + } + var members []message.AttributeValue + for _, user := range users { + members = append(members, message.AttributeValue(user.Name)) + } + return members + }}, + "description": {ldapField: "description", hideOnStarOp: true, fieldMapper: func(group *object.Group) []V { + return []V{V(group.DisplayName)} + }}, + "objectclass": {ldapField: "objectClass", hideOnStarOp: true, constantValue: []V{ + V("top"), + V("posixGroup"), + }}, +} + +var ( + AdditionalLdapUserAttributes []message.LDAPString + AdditionalLdapGroupAttributes []message.LDAPString +) func init() { - for k, v := range ldapAttributesMapping { + for _, v := range ldapUserAttributesMapping { if v.hideOnStarOp { continue } - AdditionalLdapAttributes = append(AdditionalLdapAttributes, message.LDAPString(k)) + AdditionalLdapUserAttributes = append(AdditionalLdapUserAttributes, message.LDAPString(v.ldapField)) + } + for _, v := range ldapGroupAttributesMapping { + if v.hideOnStarOp { + continue + } + AdditionalLdapGroupAttributes = append(AdditionalLdapGroupAttributes, message.LDAPString(v.ldapField)) } } @@ -307,6 +502,52 @@ func GetFilteredUsers(m *ldap.Message) (filteredUsers []*object.User, code int) } } +func GetFilteredOrganizations(m *ldap.Message) ([]*object.Organization, int) { + if m.Client.IsGlobalAdmin { + organizations, err := object.GetOrganizations("") + if err != nil { + panic(err) + } + return organizations, ldap.LDAPResultSuccess + } else if m.Client.IsOrgAdmin { + requestUserId := util.GetId(m.Client.OrgName, m.Client.UserName) + user, err := object.GetUser(requestUserId) + if err != nil { + panic(err) + } + organization, err := object.GetOrganizationByUser(user) + if err != nil { + panic(err) + } + return []*object.Organization{organization}, ldap.LDAPResultSuccess + } else { + return nil, ldap.LDAPResultInsufficientAccessRights + } +} + +func GetFilteredGroups(m *ldap.Message) ([]*object.Group, int) { + if m.Client.IsGlobalAdmin { + groups, err := object.GetGroups("") + if err != nil { + panic(err) + } + return groups, ldap.LDAPResultSuccess + } else if m.Client.IsOrgAdmin { + requestUserId := util.GetId(m.Client.OrgName, m.Client.UserName) + user, err := object.GetUser(requestUserId) + if err != nil { + panic(err) + } + groups, err := object.GetGroups(user.Owner) + if err != nil { + panic(err) + } + return groups, ldap.LDAPResultSuccess + } else { + return nil, ldap.LDAPResultInsufficientAccessRights + } +} + // get user password with hash type prefix // TODO not handle salt yet // @return {md5}5f4dcc3b5aa765d61d8327deb882cf99 @@ -330,18 +571,49 @@ func getUserPasswordWithType(user *object.User) string { return fmt.Sprintf("{%s}%s", prefix, user.Password) } -func getAttribute(attributeName string, user *object.User) message.AttributeValue { - v, ok := ldapAttributesMapping[attributeName] - if !ok { - return "" - } - return v.GetAttributeValue(user) -} - func getUserFieldFromAttribute(attributeName string) (string, error) { - v, ok := ldapAttributesMapping[attributeName] + v, ok := ldapUserAttributesMapping.CaseInsensitiveGet(attributeName) if !ok { return "", fmt.Errorf("attribute %s not supported", attributeName) } return v.GetField() } + +func searchFilterForEquality(filter message.Filter, desc string, values ...string) string { + switch f := filter.(type) { + case message.FilterAnd: + for _, child := range f { + if val := searchFilterForEquality(child, desc, values...); val != "" { + return val + } + } + case message.FilterOr: + for _, child := range f { + if val := searchFilterForEquality(child, desc, values...); val != "" { + return val + } + } + case message.FilterNot: + return searchFilterForEquality(f.Filter, desc, values...) + case message.FilterSubstrings: + // Handle FilterSubstrings case if needed + case message.FilterEqualityMatch: + if strings.EqualFold(string(f.AttributeDesc()), desc) { + for _, value := range values { + if val := string(f.AssertionValue()); val == value { + return val + } + } + } + case message.FilterGreaterOrEqual: + // Handle FilterGreaterOrEqual case if needed + case message.FilterLessOrEqual: + // Handle FilterLessOrEqual case if needed + case message.FilterPresent: + // Handle FilterPresent case if needed + case message.FilterApproxMatch: + // Handle FilterApproxMatch case if needed + } + + return "" +}