mirror of
https://github.com/casdoor/casdoor.git
synced 2025-05-23 18:54:03 +08:00

* feat: more RFC like LDAP server behaviour * Extend FieldRelationMap to support case insensitive mapping, add more fields definition * feat: Add group syncing for LDAP server
232 lines
6.8 KiB
Go
232 lines
6.8 KiB
Go
// Copyright 2022 The Casdoor Authors. All Rights Reserved.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package ldap
|
|
|
|
import (
|
|
"fmt"
|
|
"hash/fnv"
|
|
"log"
|
|
"strings"
|
|
|
|
"github.com/casdoor/casdoor/conf"
|
|
"github.com/casdoor/casdoor/object"
|
|
ldap "github.com/forestmgy/ldapserver"
|
|
"github.com/lor00x/goldap/message"
|
|
)
|
|
|
|
func StartLdapServer() {
|
|
ldapServerPort := conf.GetConfigString("ldapServerPort")
|
|
if ldapServerPort == "" || ldapServerPort == "0" {
|
|
return
|
|
}
|
|
|
|
server := ldap.NewServer()
|
|
routes := ldap.NewRouteMux()
|
|
|
|
routes.Bind(handleBind)
|
|
routes.Search(handleSearch).Label(" SEARCH****")
|
|
|
|
server.Handle(routes)
|
|
err := server.ListenAndServe("0.0.0.0:" + ldapServerPort)
|
|
if err != nil {
|
|
log.Printf("StartLdapServer() failed, err = %s", err.Error())
|
|
}
|
|
}
|
|
|
|
func handleBind(w ldap.ResponseWriter, m *ldap.Message) {
|
|
r := m.GetBindRequest()
|
|
res := ldap.NewBindResponse(ldap.LDAPResultSuccess)
|
|
|
|
if r.AuthenticationChoice() == "simple" {
|
|
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)
|
|
res.SetDiagnosticMessage(fmt.Sprintf("getNameAndOrgFromDN() error: %s", err.Error()))
|
|
w.Write(res)
|
|
return
|
|
}
|
|
|
|
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)
|
|
res.SetResultCode(ldap.LDAPResultInvalidCredentials)
|
|
res.SetDiagnosticMessage("invalid credentials ErrMsg: " + err.Error())
|
|
w.Write(res)
|
|
return
|
|
}
|
|
|
|
if bindOrg == "built-in" || bindUser.IsGlobalAdmin() {
|
|
m.Client.IsGlobalAdmin, m.Client.IsOrgAdmin = true, true
|
|
} else if bindUser.IsAdmin {
|
|
m.Client.IsOrgAdmin = true
|
|
}
|
|
|
|
m.Client.IsAuthenticated = true
|
|
m.Client.UserName = bindUsername
|
|
m.Client.OrgName = bindOrg
|
|
} else {
|
|
res.SetResultCode(ldap.LDAPResultAuthMethodNotSupported)
|
|
res.SetDiagnosticMessage("Authentication method not supported, please use Simple Authentication")
|
|
}
|
|
w.Write(res)
|
|
}
|
|
|
|
func handleSearch(w ldap.ResponseWriter, m *ldap.Message) {
|
|
res := ldap.NewSearchResultDoneResponse(ldap.LDAPResultSuccess)
|
|
if !m.Client.IsAuthenticated {
|
|
res.SetResultCode(ldap.LDAPResultUnwillingToPerform)
|
|
w.Write(res)
|
|
return
|
|
}
|
|
|
|
r := m.GetSearchRequest()
|
|
|
|
// 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
|
|
}
|
|
|
|
// Handle Stop Signal (server stop / client disconnected / Abandoned request....)
|
|
select {
|
|
case <-m.Done:
|
|
log.Print("Leaving handleSearch...")
|
|
return
|
|
default:
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
func hash(s string) uint32 {
|
|
h := fnv.New32a()
|
|
h.Write([]byte(s))
|
|
return h.Sum32()
|
|
}
|