2022-09-24 21:48:29 +08:00
// 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.
2023-04-04 20:51:28 +08:00
package ldap
2022-09-24 21:48:29 +08:00
import (
2024-12-07 21:26:07 +08:00
"crypto/tls"
2022-09-24 21:48:29 +08:00
"fmt"
2023-11-21 14:01:27 +08:00
"hash/fnv"
2022-09-24 21:48:29 +08:00
"log"
2025-07-29 21:49:39 +08:00
"strings"
2022-09-24 21:48:29 +08:00
"github.com/casdoor/casdoor/conf"
"github.com/casdoor/casdoor/object"
2024-10-08 19:18:56 +08:00
ldap "github.com/casdoor/ldapserver"
2022-09-24 21:48:29 +08:00
"github.com/lor00x/goldap/message"
)
func StartLdapServer ( ) {
2023-10-29 23:55:08 +08:00
ldapServerPort := conf . GetConfigString ( "ldapServerPort" )
2024-12-07 21:26:07 +08:00
ldapsServerPort := conf . GetConfigString ( "ldapsServerPort" )
2023-10-29 23:55:08 +08:00
2023-04-04 20:51:28 +08:00
server := ldap . NewServer ( )
2024-12-07 21:26:07 +08:00
serverSsl := ldap . NewServer ( )
2023-04-04 20:51:28 +08:00
routes := ldap . NewRouteMux ( )
2022-09-24 21:48:29 +08:00
routes . Bind ( handleBind )
routes . Search ( handleSearch ) . Label ( " SEARCH****" )
server . Handle ( routes )
2024-12-07 21:26:07 +08:00
serverSsl . Handle ( routes )
go func ( ) {
if ldapServerPort == "" || ldapServerPort == "0" {
return
}
err := server . ListenAndServe ( "0.0.0.0:" + ldapServerPort )
if err != nil {
log . Printf ( "StartLdapServer() failed, err = %s" , err . Error ( ) )
}
} ( )
go func ( ) {
if ldapsServerPort == "" || ldapsServerPort == "0" {
return
}
ldapsCertId := conf . GetConfigString ( "ldapsCertId" )
if ldapsCertId == "" {
return
}
config , err := getTLSconfig ( ldapsCertId )
if err != nil {
log . Printf ( "StartLdapsServer() failed, err = %s" , err . Error ( ) )
return
}
secureConn := func ( s * ldap . Server ) {
s . Listener = tls . NewListener ( s . Listener , config )
}
err = serverSsl . ListenAndServe ( "0.0.0.0:" + ldapsServerPort , secureConn )
if err != nil {
log . Printf ( "StartLdapsServer() failed, err = %s" , err . Error ( ) )
}
} ( )
}
func getTLSconfig ( ldapsCertId string ) ( * tls . Config , error ) {
rawCert , err := object . GetCert ( ldapsCertId )
2023-04-04 20:51:28 +08:00
if err != nil {
2024-12-07 21:26:07 +08:00
return nil , err
}
if rawCert == nil {
return nil , fmt . Errorf ( "cert is empty" )
2023-04-04 20:51:28 +08:00
}
2024-12-07 21:26:07 +08:00
cert , err := tls . X509KeyPair ( [ ] byte ( rawCert . Certificate ) , [ ] byte ( rawCert . PrivateKey ) )
if err != nil {
return & tls . Config { } , err
}
return & tls . Config {
MinVersion : tls . VersionTLS10 ,
MaxVersion : tls . VersionTLS13 ,
Certificates : [ ] tls . Certificate { cert } ,
} , nil
2022-09-24 21:48:29 +08:00
}
2023-04-04 20:51:28 +08:00
func handleBind ( w ldap . ResponseWriter , m * ldap . Message ) {
2022-09-24 21:48:29 +08:00
r := m . GetBindRequest ( )
2023-04-04 20:51:28 +08:00
res := ldap . NewBindResponse ( ldap . LDAPResultSuccess )
2022-09-24 21:48:29 +08:00
if r . AuthenticationChoice ( ) == "simple" {
2024-01-15 13:58:33 +08:00
bindUsername , bindOrg , err := getNameAndOrgFromDN ( string ( r . Name ( ) ) )
2023-11-19 19:58:07 +08:00
if err != nil {
log . Printf ( "getNameAndOrgFromDN() error: %s" , err . Error ( ) )
2023-04-04 20:51:28 +08:00
res . SetResultCode ( ldap . LDAPResultInvalidDNSyntax )
2023-11-19 19:58:07 +08:00
res . SetDiagnosticMessage ( fmt . Sprintf ( "getNameAndOrgFromDN() error: %s" , err . Error ( ) ) )
2022-09-24 21:48:29 +08:00
w . Write ( res )
return
}
2023-04-04 20:51:28 +08:00
2024-01-15 13:58:33 +08:00
bindPassword := string ( r . AuthenticationSimple ( ) )
2024-07-18 15:04:17 +02:00
enableCaptcha := false
isSigninViaLdap := false
isPasswordWithLdapEnabled := false
if bindPassword != "" {
isPasswordWithLdapEnabled = true
}
bindUser , err := object . CheckUserPassword ( bindOrg , bindUsername , bindPassword , "en" , enableCaptcha , isSigninViaLdap , isPasswordWithLdapEnabled )
2023-11-19 19:58:07 +08:00
if err != nil {
2022-09-24 21:48:29 +08:00
log . Printf ( "Bind failed User=%s, Pass=%#v, ErrMsg=%s" , string ( r . Name ( ) ) , r . Authentication ( ) , err )
2023-04-04 20:51:28 +08:00
res . SetResultCode ( ldap . LDAPResultInvalidCredentials )
2023-11-19 19:58:07 +08:00
res . SetDiagnosticMessage ( "invalid credentials ErrMsg: " + err . Error ( ) )
2022-09-24 21:48:29 +08:00
w . Write ( res )
return
}
2023-04-04 20:51:28 +08:00
2023-08-19 12:23:15 +08:00
if bindOrg == "built-in" || bindUser . IsGlobalAdmin ( ) {
2022-09-24 21:48:29 +08:00
m . Client . IsGlobalAdmin , m . Client . IsOrgAdmin = true , true
2023-04-04 20:51:28 +08:00
} else if bindUser . IsAdmin {
2022-09-24 21:48:29 +08:00
m . Client . IsOrgAdmin = true
}
2023-04-04 20:51:28 +08:00
2022-09-24 21:48:29 +08:00
m . Client . IsAuthenticated = true
2023-04-04 20:51:28 +08:00
m . Client . UserName = bindUsername
m . Client . OrgName = bindOrg
2022-09-24 21:48:29 +08:00
} else {
2023-04-04 20:51:28 +08:00
res . SetResultCode ( ldap . LDAPResultAuthMethodNotSupported )
2023-11-19 19:58:07 +08:00
res . SetDiagnosticMessage ( "Authentication method not supported, please use Simple Authentication" )
2022-09-24 21:48:29 +08:00
}
w . Write ( res )
}
2023-04-04 20:51:28 +08:00
func handleSearch ( w ldap . ResponseWriter , m * ldap . Message ) {
res := ldap . NewSearchResultDoneResponse ( ldap . LDAPResultSuccess )
2022-09-24 21:48:29 +08:00
if ! m . Client . IsAuthenticated {
2023-04-04 20:51:28 +08:00
res . SetResultCode ( ldap . LDAPResultUnwillingToPerform )
2022-09-24 21:48:29 +08:00
w . Write ( res )
return
}
2023-04-04 20:51:28 +08:00
2022-09-24 21:48:29 +08:00
r := m . GetSearchRequest ( )
2023-04-04 20:51:28 +08:00
2022-09-24 21:48:29 +08:00
// Handle Stop Signal (server stop / client disconnected / Abandoned request....)
select {
case <- m . Done :
log . Print ( "Leaving handleSearch..." )
return
default :
}
2023-04-04 20:51:28 +08:00
2025-07-31 01:23:25 +08:00
if strings . EqualFold ( r . FilterString ( ) , "(objectClass=*)" ) && ( string ( r . BaseObject ( ) ) == "" || strings . EqualFold ( string ( r . BaseObject ( ) ) , "cn=Subschema" ) ) {
handleRootSearch ( w , & r , & res , m )
return
}
2025-07-29 21:49:39 +08:00
var isGroupSearch bool = false
filter := r . Filter ( )
if eq , ok := filter . ( message . FilterEqualityMatch ) ; ok && strings . EqualFold ( string ( eq . AttributeDesc ( ) ) , "objectClass" ) && strings . EqualFold ( string ( eq . AssertionValue ( ) ) , "posixGroup" ) {
isGroupSearch = true
}
if isGroupSearch {
groups , code := GetFilteredGroups ( m , string ( r . BaseObject ( ) ) , r . FilterString ( ) )
if code != ldap . LDAPResultSuccess {
res . SetResultCode ( code )
w . Write ( res )
return
}
for _ , group := range groups {
dn := fmt . Sprintf ( "cn=%s,%s" , group . Name , string ( r . BaseObject ( ) ) )
e := ldap . NewSearchResultEntry ( dn )
e . AddAttribute ( "cn" , message . AttributeValue ( group . Name ) )
gidNumberStr := fmt . Sprintf ( "%v" , hash ( group . Name ) )
e . AddAttribute ( "gidNumber" , message . AttributeValue ( gidNumberStr ) )
users := object . GetGroupUsersWithoutError ( group . GetId ( ) )
for _ , user := range users {
e . AddAttribute ( "memberUid" , message . AttributeValue ( user . Name ) )
}
e . AddAttribute ( "objectClass" , "posixGroup" )
w . Write ( e )
}
w . Write ( res )
return
}
2024-01-15 13:58:33 +08:00
users , code := GetFilteredUsers ( m )
if code != ldap . LDAPResultSuccess {
res . SetResultCode ( code )
w . Write ( res )
return
}
2023-04-04 20:51:28 +08:00
2024-01-15 13:58:33 +08:00
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 ) )
2024-07-20 20:55:42 +03:30
for _ , group := range user . Groups {
e . AddAttribute ( ldapMemberOfAttr , message . AttributeValue ( group ) )
}
2024-01-15 13:58:33 +08:00
attrs := r . Attributes ( )
for _ , attr := range attrs {
if string ( attr ) == "*" {
attrs = AdditionalLdapAttributes
break
2023-11-22 23:52:40 +08:00
}
}
2024-01-15 13:58:33 +08:00
for _ , attr := range attrs {
e . AddAttribute ( message . AttributeDescription ( attr ) , getAttribute ( string ( attr ) , user ) )
2024-11-26 09:13:05 +08:00
if string ( attr ) == "title" {
2024-01-15 13:58:33 +08:00
e . AddAttribute ( message . AttributeDescription ( attr ) , getAttribute ( "title" , user ) )
2023-05-12 13:03:43 +08:00
}
2023-04-13 14:12:31 +08:00
}
2024-01-15 13:58:33 +08:00
w . Write ( e )
2022-09-24 21:48:29 +08:00
}
w . Write ( res )
}
2023-11-21 14:01:27 +08:00
2025-07-31 01:23:25 +08:00
func handleRootSearch ( w ldap . ResponseWriter , r * message . SearchRequest , res * message . SearchResultDone , m * ldap . Message ) {
if len ( r . Attributes ( ) ) == 0 {
w . Write ( res )
return
}
firstAttr := string ( r . Attributes ( ) [ 0 ] )
if string ( r . BaseObject ( ) ) == "" {
// Handle special root DSE requests
if strings . EqualFold ( firstAttr , "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 strings . EqualFold ( firstAttr , "subschemaSubentry" ) {
e := ldap . NewSearchResultEntry ( string ( r . BaseObject ( ) ) )
e . AddAttribute ( "subschemaSubentry" , message . AttributeValue ( "cn=Subschema" ) )
w . Write ( e )
}
} else if strings . EqualFold ( firstAttr , "objectclasses" ) && strings . EqualFold ( 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 )
}
2023-11-21 14:01:27 +08:00
func hash ( s string ) uint32 {
h := fnv . New32a ( )
h . Write ( [ ] byte ( s ) )
return h . Sum32 ( )
}