feat: revert PR: "feat: more RFC like LDAP server behaviour" (#2611)

This commit is contained in:
hsluoyz 2024-01-15 13:58:33 +08:00 committed by GitHub
parent 1161310f81
commit d92b072ed0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 68 additions and 423 deletions

View File

@ -18,7 +18,6 @@ import (
"fmt" "fmt"
"hash/fnv" "hash/fnv"
"log" "log"
"strings"
"github.com/casdoor/casdoor/conf" "github.com/casdoor/casdoor/conf"
"github.com/casdoor/casdoor/object" "github.com/casdoor/casdoor/object"
@ -50,17 +49,7 @@ func handleBind(w ldap.ResponseWriter, m *ldap.Message) {
res := ldap.NewBindResponse(ldap.LDAPResultSuccess) res := ldap.NewBindResponse(ldap.LDAPResultSuccess)
if r.AuthenticationChoice() == "simple" { if r.AuthenticationChoice() == "simple" {
bindDN := string(r.Name()) bindUsername, bindOrg, err := getNameAndOrgFromDN(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 { if err != nil {
log.Printf("getNameAndOrgFromDN() error: %s", err.Error()) log.Printf("getNameAndOrgFromDN() error: %s", err.Error())
res.SetResultCode(ldap.LDAPResultInvalidDNSyntax) res.SetResultCode(ldap.LDAPResultInvalidDNSyntax)
@ -69,6 +58,7 @@ func handleBind(w ldap.ResponseWriter, m *ldap.Message) {
return return
} }
bindPassword := string(r.AuthenticationSimple())
bindUser, err := object.CheckUserPassword(bindOrg, bindUsername, bindPassword, "en") bindUser, err := object.CheckUserPassword(bindOrg, bindUsername, bindPassword, "en")
if err != nil { if err != nil {
log.Printf("Bind failed User=%s, Pass=%#v, ErrMsg=%s", string(r.Name()), r.Authentication(), err) log.Printf("Bind failed User=%s, Pass=%#v, ErrMsg=%s", string(r.Name()), r.Authentication(), err)
@ -103,46 +93,7 @@ func handleSearch(w ldap.ResponseWriter, m *ldap.Message) {
} }
r := m.GetSearchRequest() 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) w.Write(res)
return return
} }
@ -155,72 +106,38 @@ func handleSearch(w ldap.ResponseWriter, m *ldap.Message) {
default: default:
} }
objectClass := searchFilterForEquality(r.Filter(), "objectClass", "posixAccount", "posixGroup") users, code := GetFilteredUsers(m)
switch objectClass { if code != ldap.LDAPResultSuccess {
case "posixAccount": res.SetResultCode(code)
users, code := GetFilteredUsers(m) w.Write(res)
if code != ldap.LDAPResultSuccess { return
res.SetResultCode(code)
w.Write(res)
return
}
// 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())
} }
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))
}
}
w.Write(e)
}
w.Write(res) w.Write(res)
} }

View File

@ -18,7 +18,6 @@ import (
"fmt" "fmt"
"log" "log"
"strings" "strings"
"time"
"github.com/casdoor/casdoor/object" "github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util" "github.com/casdoor/casdoor/util"
@ -29,259 +28,65 @@ import (
"github.com/xorm-io/builder" "github.com/xorm-io/builder"
) )
type V = message.AttributeValue type AttributeMapper func(user *object.User) message.AttributeValue
type UserAttributeMapper func(user *object.User) []V type FieldRelation struct {
type UserFieldRelation struct {
userField string userField string
ldapField string
notSearchable bool notSearchable bool
hideOnStarOp bool hideOnStarOp bool
fieldMapper UserAttributeMapper fieldMapper AttributeMapper
constantValue []V
} }
func (rel UserFieldRelation) GetField() (string, error) { func (rel FieldRelation) GetField() (string, error) {
if rel.notSearchable { if rel.notSearchable {
return "", fmt.Errorf("attribute %s not supported", rel.userField) return "", fmt.Errorf("attribute %s not supported", rel.userField)
} }
return rel.userField, nil return rel.userField, nil
} }
func (rel UserFieldRelation) GetAttributeValues(user *object.User) []V { func (rel FieldRelation) GetAttributeValue(user *object.User) message.AttributeValue {
if rel.constantValue != nil && rel.fieldMapper == nil {
return rel.constantValue
}
return rel.fieldMapper(user) return rel.fieldMapper(user)
} }
type UserFieldRelationMap map[string]UserFieldRelation var ldapAttributesMapping = map[string]FieldRelation{
"cn": {userField: "name", hideOnStarOp: true, fieldMapper: func(user *object.User) message.AttributeValue {
func (m UserFieldRelationMap) CaseInsensitiveGet(key string) (UserFieldRelation, bool) { return message.AttributeValue(user.Name)
lowerKey := strings.ToLower(key)
ret, ok := m[lowerKey]
return ret, ok
}
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 { "uid": {userField: "name", hideOnStarOp: true, fieldMapper: func(user *object.User) message.AttributeValue {
return []V{V(user.Name)} return message.AttributeValue(user.Name)
}}, }},
"displayname": {ldapField: "displayName", userField: "displayName", fieldMapper: func(user *object.User) []V { "displayname": {userField: "displayName", fieldMapper: func(user *object.User) message.AttributeValue {
return []V{V(user.DisplayName)} return message.AttributeValue(user.DisplayName)
}}, }},
"email": {ldapField: "email", userField: "email", fieldMapper: func(user *object.User) []V { "email": {userField: "email", fieldMapper: func(user *object.User) message.AttributeValue {
return []V{V(user.Email)} return message.AttributeValue(user.Email)
}}, }},
"mail": {ldapField: "mail", userField: "email", fieldMapper: func(user *object.User) []V { "mail": {userField: "email", fieldMapper: func(user *object.User) message.AttributeValue {
return []V{V(user.Email)} return message.AttributeValue(user.Email)
}}, }},
"mobile": {ldapField: "mobile", userField: "phone", fieldMapper: func(user *object.User) []V { "mobile": {userField: "phone", fieldMapper: func(user *object.User) message.AttributeValue {
return []V{V(user.Phone)} return message.AttributeValue(user.Phone)
}}, }},
"telephonenumber": {ldapField: "telephoneNumber", userField: "phone", fieldMapper: func(user *object.User) []V { "title": {userField: "tag", fieldMapper: func(user *object.User) message.AttributeValue {
return []V{V(user.Phone)} return message.AttributeValue(user.Tag)
}}, }},
"postaladdress": {ldapField: "postalAddress", userField: "address", fieldMapper: func(user *object.User) []V { "userPassword": {
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", userField: "userPassword",
notSearchable: true, notSearchable: true,
fieldMapper: func(user *object.User) []V { fieldMapper: func(user *object.User) message.AttributeValue {
return []V{V(getUserPasswordWithType(user))} return message.AttributeValue(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{ var AdditionalLdapAttributes []message.LDAPString
"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() { func init() {
for _, v := range ldapUserAttributesMapping { for k, v := range ldapAttributesMapping {
if v.hideOnStarOp { if v.hideOnStarOp {
continue continue
} }
AdditionalLdapUserAttributes = append(AdditionalLdapUserAttributes, message.LDAPString(v.ldapField)) AdditionalLdapAttributes = append(AdditionalLdapAttributes, message.LDAPString(k))
}
for _, v := range ldapGroupAttributesMapping {
if v.hideOnStarOp {
continue
}
AdditionalLdapGroupAttributes = append(AdditionalLdapGroupAttributes, message.LDAPString(v.ldapField))
} }
} }
@ -502,52 +307,6 @@ 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 // get user password with hash type prefix
// TODO not handle salt yet // TODO not handle salt yet
// @return {md5}5f4dcc3b5aa765d61d8327deb882cf99 // @return {md5}5f4dcc3b5aa765d61d8327deb882cf99
@ -571,49 +330,18 @@ func getUserPasswordWithType(user *object.User) string {
return fmt.Sprintf("{%s}%s", prefix, user.Password) 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) { func getUserFieldFromAttribute(attributeName string) (string, error) {
v, ok := ldapUserAttributesMapping.CaseInsensitiveGet(attributeName) v, ok := ldapAttributesMapping[attributeName]
if !ok { if !ok {
return "", fmt.Errorf("attribute %s not supported", attributeName) return "", fmt.Errorf("attribute %s not supported", attributeName)
} }
return v.GetField() 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 ""
}