Page Menu
Home
c4science
Search
Configure Global Search
Log In
Files
F93466354
PhutilAuthAdapterLDAP.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Subscribers
None
File Metadata
Details
File Info
Storage
Attached
Created
Fri, Nov 29, 00:05
Size
13 KB
Mime Type
text/x-php
Expires
Sun, Dec 1, 00:05 (2 d)
Engine
blob
Format
Raw Data
Handle
22646343
Attached To
rPHU libphutil
PhutilAuthAdapterLDAP.php
View Options
<?php
/**
* Retrieve identify information from LDAP accounts.
*/
final
class
PhutilAuthAdapterLDAP
extends
PhutilAuthAdapter
{
private
$hostname
;
private
$port
=
389
;
private
$baseDistinguishedName
;
private
$searchAttributes
=
array
();
private
$usernameAttribute
;
private
$realNameAttributes
=
array
();
private
$ldapVersion
=
3
;
private
$ldapReferrals
;
private
$ldapStartTLS
;
private
$anonymousUsername
;
private
$anonymousPassword
;
private
$activeDirectoryDomain
;
private
$alwaysSearch
;
private
$loginUsername
;
private
$loginPassword
;
private
$ldapUserData
;
private
$ldapConnection
;
public
function
getAdapterType
()
{
return
'ldap'
;
}
public
function
setHostname
(
$host
)
{
$this
->
hostname
=
$host
;
return
$this
;
}
public
function
setPort
(
$port
)
{
$this
->
port
=
$port
;
return
$this
;
}
public
function
getAdapterDomain
()
{
return
'self'
;
}
public
function
setBaseDistinguishedName
(
$base_distinguished_name
)
{
$this
->
baseDistinguishedName
=
$base_distinguished_name
;
return
$this
;
}
public
function
setSearchAttributes
(
array
$search_attributes
)
{
$this
->
searchAttributes
=
$search_attributes
;
return
$this
;
}
public
function
setUsernameAttribute
(
$username_attribute
)
{
$this
->
usernameAttribute
=
$username_attribute
;
return
$this
;
}
public
function
setRealNameAttributes
(
array
$attributes
)
{
$this
->
realNameAttributes
=
$attributes
;
return
$this
;
}
public
function
setLDAPVersion
(
$ldap_version
)
{
$this
->
ldapVersion
=
$ldap_version
;
return
$this
;
}
public
function
setLDAPReferrals
(
$ldap_referrals
)
{
$this
->
ldapReferrals
=
$ldap_referrals
;
return
$this
;
}
public
function
setLDAPStartTLS
(
$ldap_start_tls
)
{
$this
->
ldapStartTLS
=
$ldap_start_tls
;
return
$this
;
}
public
function
setAnonymousUsername
(
$anonymous_username
)
{
$this
->
anonymousUsername
=
$anonymous_username
;
return
$this
;
}
public
function
setAnonymousPassword
(
PhutilOpaqueEnvelope
$anonymous_password
)
{
$this
->
anonymousPassword
=
$anonymous_password
;
return
$this
;
}
public
function
setLoginUsername
(
$login_username
)
{
$this
->
loginUsername
=
$login_username
;
return
$this
;
}
public
function
setLoginPassword
(
PhutilOpaqueEnvelope
$login_password
)
{
$this
->
loginPassword
=
$login_password
;
return
$this
;
}
public
function
setActiveDirectoryDomain
(
$domain
)
{
$this
->
activeDirectoryDomain
=
$domain
;
return
$this
;
}
public
function
setAlwaysSearch
(
$always_search
)
{
$this
->
alwaysSearch
=
$always_search
;
return
$this
;
}
public
function
getAccountID
()
{
return
$this
->
readLDAPRecordAccountID
(
$this
->
getLDAPUserData
());
}
public
function
getAccountName
()
{
return
$this
->
readLDAPRecordAccountName
(
$this
->
getLDAPUserData
());
}
public
function
getAccountRealName
()
{
return
$this
->
readLDAPRecordRealName
(
$this
->
getLDAPUserData
());
}
public
function
getAccountEmail
()
{
return
$this
->
readLDAPRecordEmail
(
$this
->
getLDAPUserData
());
}
public
function
readLDAPRecordAccountID
(
array
$record
)
{
$key
=
$this
->
usernameAttribute
;
if
(!
strlen
(
$key
))
{
$key
=
head
(
$this
->
searchAttributes
);
}
return
$this
->
readLDAPData
(
$record
,
$key
);
}
public
function
readLDAPRecordAccountName
(
array
$record
)
{
return
$this
->
readLDAPRecordAccountID
(
$record
);
}
public
function
readLDAPRecordRealName
(
array
$record
)
{
$parts
=
array
();
foreach
(
$this
->
realNameAttributes
as
$attribute
)
{
$parts
[]
=
$this
->
readLDAPData
(
$record
,
$attribute
);
}
$parts
=
array_filter
(
$parts
);
if
(
$parts
)
{
return
implode
(
' '
,
$parts
);
}
return
null
;
}
public
function
readLDAPRecordEmail
(
array
$record
)
{
return
$this
->
readLDAPData
(
$record
,
'mail'
);
}
private
function
getLDAPUserData
()
{
if
(
$this
->
ldapUserData
===
null
)
{
$this
->
ldapUserData
=
$this
->
loadLDAPUserData
();
}
return
$this
->
ldapUserData
;
}
private
function
readLDAPData
(
array
$data
,
$key
,
$default
=
null
)
{
$list
=
idx
(
$data
,
$key
);
if
(
$list
===
null
)
{
// At least in some cases (and maybe in all cases) the results from
// ldap_search() are keyed in lowercase. If we missed on the first
// try, retry with a lowercase key.
$list
=
idx
(
$data
,
phutil_utf8_strtolower
(
$key
));
}
// NOTE: In most cases, the property is an array, like:
//
// array(
// 'count' => 1,
// 0 => 'actual-value-we-want',
// )
//
// However, in at least the case of 'dn', the property is a bare string.
if
(
is_scalar
(
$list
)
&&
strlen
(
$list
))
{
return
$list
;
}
else
if
(
is_array
(
$list
))
{
return
$list
[
0
];
}
else
{
return
$default
;
}
}
private
function
formatLDAPAttributeSearch
(
$attribute
,
$login_user
)
{
// If the attribute contains the literal token "${login}", treat it as a
// query and substitute the user's login name for the token.
if
(
strpos
(
$attribute
,
'${login}'
)
!==
false
)
{
$escaped_user
=
ldap_sprintf
(
'%S'
,
$login_user
);
$attribute
=
str_replace
(
'${login}'
,
$escaped_user
,
$attribute
);
return
$attribute
;
}
// Otherwise, treat it as a simple attribute search.
return
ldap_sprintf
(
'%Q=%S'
,
$attribute
,
$login_user
);
}
private
function
loadLDAPUserData
()
{
$conn
=
$this
->
establishConnection
();
$login_user
=
$this
->
loginUsername
;
$login_pass
=
$this
->
loginPassword
;
if
(
$this
->
shouldBindWithoutIdentity
())
{
$distinguished_name
=
null
;
$search_query
=
null
;
foreach
(
$this
->
searchAttributes
as
$attribute
)
{
$search_query
=
$this
->
formatLDAPAttributeSearch
(
$attribute
,
$login_user
);
$record
=
$this
->
searchLDAPForRecord
(
$search_query
);
if
(
$record
)
{
$distinguished_name
=
$this
->
readLDAPData
(
$record
,
'dn'
);
break
;
}
}
if
(
$distinguished_name
===
null
)
{
throw
new
PhutilAuthCredentialException
();
}
}
else
{
$search_query
=
$this
->
formatLDAPAttributeSearch
(
head
(
$this
->
searchAttributes
),
$login_user
);
if
(
$this
->
activeDirectoryDomain
)
{
$distinguished_name
=
ldap_sprintf
(
'%s@%Q'
,
$login_user
,
$this
->
activeDirectoryDomain
);
}
else
{
$distinguished_name
=
ldap_sprintf
(
'%Q,%Q'
,
$search_query
,
$this
->
baseDistinguishedName
);
}
}
$this
->
bindLDAP
(
$conn
,
$distinguished_name
,
$login_pass
);
$result
=
$this
->
searchLDAPForRecord
(
$search_query
);
if
(!
$result
)
{
// This is unusual (since the bind succeeded) but we've seen it at least
// once in the wild, where the anonymous user is allowed to search but
// the credentialed user is not.
// If we don't have anonymous credentials, raise an explicit exception
// here since we'll fail a typehint if we don't return an array anyway
// and this is a more useful error.
// If we do have anonymous credentials, we'll rebind and try the search
// again below. Doing this automatically means things work correctly more
// often without requiring additional configuration.
if
(!
$this
->
shouldBindWithoutIdentity
())
{
// No anonymous credentials, so we just fail here.
throw
new
Exception
(
pht
(
'LDAP: Failed to retrieve record for user "%s" when searching. '
.
'Credentialed users may not be able to search your LDAP server. '
.
'Try configuring anonymous credentials or fully anonymous binds.'
,
$login_user
));
}
else
{
// Rebind as anonymous and try the search again.
$user
=
$this
->
anonymousUsername
;
$pass
=
$this
->
anonymousPassword
;
$this
->
bindLDAP
(
$conn
,
$user
,
$pass
);
$result
=
$this
->
searchLDAPForRecord
(
$search_query
);
if
(!
$result
)
{
throw
new
Exception
(
pht
(
'LDAP: Failed to retrieve record for user "%s" when searching '
.
'with both user and anonymous credentials.'
,
$login_user
));
}
}
}
return
$result
;
}
private
function
establishConnection
()
{
if
(!
$this
->
ldapConnection
)
{
$host
=
$this
->
hostname
;
$port
=
$this
->
port
;
$profiler
=
PhutilServiceProfiler
::
getInstance
();
$call_id
=
$profiler
->
beginServiceCall
(
array
(
'type'
=>
'ldap'
,
'call'
=>
'connect'
,
'host'
=>
$host
,
'port'
=>
$this
->
port
,
));
$conn
=
@
ldap_connect
(
$host
,
$this
->
port
);
$profiler
->
endServiceCall
(
$call_id
,
array
(
'ok'
=>
(
bool
)
$conn
,
));
if
(!
$conn
)
{
throw
new
Exception
(
"Unable to connect to LDAP server ({$host}:{$port})."
);
}
$options
=
array
(
LDAP_OPT_PROTOCOL_VERSION
=>
(
int
)
$this
->
ldapVersion
,
LDAP_OPT_REFERRALS
=>
(
int
)
$this
->
ldapReferrals
,
);
foreach
(
$options
as
$name
=>
$value
)
{
$ok
=
@
ldap_set_option
(
$conn
,
$name
,
$value
);
if
(!
$ok
)
{
$this
->
raiseConnectionException
(
$conn
,
pht
(
"Unable to set LDAP option '%s' to value '%s'!"
,
$name
,
$value
));
}
}
if
(
$this
->
ldapStartTLS
)
{
$profiler
=
PhutilServiceProfiler
::
getInstance
();
$call_id
=
$profiler
->
beginServiceCall
(
array
(
'type'
=>
'ldap'
,
'call'
=>
'start-tls'
,
));
// NOTE: This boils down to a function call to ldap_start_tls_s() in
// C, which is a service call.
$ok
=
@
ldap_start_tls
(
$conn
);
$profiler
->
endServiceCall
(
$call_id
,
array
());
if
(!
$ok
)
{
$this
->
raiseConnectionException
(
$conn
,
pht
(
'Unable to start TLS connection when connecting to LDAP.'
));
}
}
if
(
$this
->
shouldBindWithoutIdentity
())
{
$user
=
$this
->
anonymousUsername
;
$pass
=
$this
->
anonymousPassword
;
$this
->
bindLDAP
(
$conn
,
$user
,
$pass
);
}
$this
->
ldapConnection
=
$conn
;
}
return
$this
->
ldapConnection
;
}
private
function
searchLDAPForRecord
(
$dn
)
{
$conn
=
$this
->
establishConnection
();
$results
=
$this
->
searchLDAP
(
'%Q'
,
$dn
);
if
(!
$results
)
{
return
null
;
}
if
(
count
(
$results
)
>
1
)
{
throw
new
Exception
(
pht
(
'LDAP record query returned more than one result. The query must '
.
'uniquely identify a record.'
));
}
return
head
(
$results
);
}
public
function
searchLDAP
(
$pattern
/* ... */
)
{
$args
=
func_get_args
();
$query
=
call_user_func_array
(
'ldap_sprintf'
,
$args
);
$conn
=
$this
->
establishConnection
();
$profiler
=
PhutilServiceProfiler
::
getInstance
();
$call_id
=
$profiler
->
beginServiceCall
(
array
(
'type'
=>
'ldap'
,
'call'
=>
'search'
,
'dn'
=>
$this
->
baseDistinguishedName
,
'query'
=>
$query
,
));
$result
=
@
ldap_search
(
$conn
,
$this
->
baseDistinguishedName
,
$query
);
$profiler
->
endServiceCall
(
$call_id
,
array
());
if
(!
$result
)
{
$this
->
raiseConnectionException
(
$conn
,
pht
(
'LDAP search failed.'
));
}
$entries
=
@
ldap_get_entries
(
$conn
,
$result
);
if
(!
$entries
)
{
$this
->
raiseConnectionException
(
$conn
,
pht
(
'Failed to get LDAP entries from search result.'
));
}
$results
=
array
();
for
(
$ii
=
0
;
$ii
<
$entries
[
'count'
];
$ii
++)
{
$results
[]
=
$entries
[
$ii
];
}
return
$results
;
}
private
function
raiseConnectionException
(
$conn
,
$message
)
{
$errno
=
@
ldap_errno
(
$conn
);
$error
=
@
ldap_error
(
$conn
);
// This is `LDAP_INVALID_CREDENTIALS`.
if
(
$errno
==
49
)
{
throw
new
PhutilAuthCredentialException
();
}
if
(
$errno
||
$error
)
{
$full_message
=
pht
(
"LDAP Exception: %s
\n
LDAP Error #%d: %s"
,
$message
,
$errno
,
$error
);
}
else
{
$full_message
=
pht
(
'LDAP Exception: %s'
,
$message
);
}
throw
new
Exception
(
$full_message
);
}
private
function
bindLDAP
(
$conn
,
$user
,
PhutilOpaqueEnvelope
$pass
)
{
$profiler
=
PhutilServiceProfiler
::
getInstance
();
$call_id
=
$profiler
->
beginServiceCall
(
array
(
'type'
=>
'ldap'
,
'call'
=>
'bind'
,
'user'
=>
$user
,
));
// NOTE: ldap_bind() dumps cleartext passwords into logs by default. Keep
// it quiet.
if
(
strlen
(
$user
))
{
$ok
=
@
ldap_bind
(
$conn
,
$user
,
$pass
->
openEnvelope
());
}
else
{
$ok
=
@
ldap_bind
(
$conn
);
}
$profiler
->
endServiceCall
(
$call_id
,
array
());
if
(!
$ok
)
{
if
(
strlen
(
$user
))
{
$this
->
raiseConnectionException
(
$conn
,
pht
(
'Failed to bind to LDAP server (as user "%s").'
,
$user
));
}
else
{
$this
->
raiseConnectionException
(
$conn
,
pht
(
'Failed to bind to LDAP server (without username).'
));
}
}
}
/**
* Determine if this adapter should attempt to bind to the LDAP server
* without a user identity.
*
* Generally, we can bind directly if we have a username/password, or if the
* "Always Search" flag is set, indicating that the empty username and
* password are sufficient.
*
* @return bool True if the adapter should perform binds without identity.
*/
private
function
shouldBindWithoutIdentity
()
{
return
$this
->
alwaysSearch
||
strlen
(
$this
->
anonymousUsername
);
}
}
Event Timeline
Log In to Comment