(updated at the end)
I am having trouble registering any user from a specific LDAP as even after finding the user from LDAP, when it tries to register, the username is null, so it triggers a violation constraint and rollsback. Am using Sonata User Bundle, FR3D LDAP Bundle and FOS User Bundle.
dev.log
[2018-01-30 16:06:09] doctrine.DEBUG: SELECT t0.username AS username_1, t0.username_canonical AS username_canonical_2, t0.email AS email_3, t0.email_canonical AS email_canonical_4, t0.enabled AS enabled_5, t0.salt AS salt_6, t0.password AS password_7, t0.last_login AS last_login_8, t0.confirmation_token AS confirmation_token_9, t0.password_requested_at AS password_requested_at_10, t0.roles AS roles_11, t0.created_at AS created_at_12, t0.updated_at AS updated_at_13, t0.date_of_birth AS date_of_birth_14, t0.firstname AS firstname_15, t0.lastname AS lastname_16, t0.website AS website_17, t0.biography AS biography_18, t0.gender AS gender_19, t0.locale AS locale_20, t0.timezone AS timezone_21, t0.phone AS phone_22, t0.facebook_uid AS facebook_uid_23, t0.facebook_name AS facebook_name_24, t0.facebook_data AS facebook_data_25, t0.twitter_uid AS twitter_uid_26, t0.twitter_name AS twitter_name_27, t0.twitter_data AS twitter_data_28, t0.gplus_uid AS gplus_uid_29, t0.gplus_name AS gplus_name_30, t0.gplus_data AS gplus_data_31, t0.token AS token_32, t0.two_step_code AS two_step_code_33, t0.ab_number AS ab_number_34, t0.title AS title_35, t0.hire_date AS hire_date_36, t0.employment_status AS employment_status_37, t0.departure_date AS departure_date_38, t0.departure_reason AS departure_reason_39, t0.local_balance AS local_balance_40, t0.sick_balance AS sick_balance_41, t0.frozen_local_balance AS frozen_local_balance_42, t0.carry_forward_local_balance AS carry_forward_local_balance_43, t0.frozen_carry_forward_local_balance AS frozen_carry_forward_local_balance_44, t0.is_no_probation_leaves AS is_no_probation_leaves_45, t0.dn AS dn_46, t0.created_by AS created_by_47, t0.updated_by AS updated_by_48, t0.id AS id_49, t0.job_title_id AS job_title_id_50, t0.department_id AS department_id_51, t0.project_id AS project_id_52, t0.business_unit_id AS business_unit_id_53 FROM fos_user_user t0 WHERE t0.username_canonical = ? LIMIT 1 ["gpotest"] []
[2018-01-30 16:06:09] ldap_driver.DEBUG: ldap_search(dc=example,dc=mu, (&(&(ObjectClass=person))(sAMAccountName=gpotest)), [array]) {"action":"ldap_search","base_dn":"dc=example,dc=mu","filter":"(&(&(ObjectClass=person))(sAMAccountName=gpotest))","attributes":[]} []
[2018-01-30 16:06:09] security.INFO: User gpotest found on LDAP {"action":"loadUserByUsername","username":"gpotest","result":"found"} []
[2018-01-30 16:06:09] ldap_driver.DEBUG: ldap_bind(CN=gpotest,OU=Test,DC=example,DC=mu, ****) {"action":"ldap_bind","bind_rdn":"CN=gpotest,OU=Test,DC=example,DC=mu"} []
[2018-01-30 16:06:09] security.INFO: User has been authenticated successfully. {"username":null} []
[2018-01-30 16:06:09] doctrine.DEBUG: "START TRANSACTION" [] []
[2018-01-30 16:06:09] doctrine.DEBUG: INSERT INTO fos_user_user (username, username_canonical, email, email_canonical, enabled, salt, password, last_login, confirmation_token, password_requested_at, roles, created_at, updated_at, date_of_birth, firstname, lastname, website, biography, gender, locale, timezone, phone, facebook_uid, facebook_name, facebook_data, twitter_uid, twitter_name, twitter_data, gplus_uid, gplus_name, gplus_data, token, two_step_code, ab_number, title, hire_date, employment_status, departure_date, departure_reason, local_balance, sick_balance, frozen_local_balance, carry_forward_local_balance, frozen_carry_forward_local_balance, is_no_probation_leaves, dn, created_by, updated_by, job_title_id, department_id, project_id, business_unit_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) {"1":null,"2":null,"3":null,"4":null,"5":true,"6":null,"7":"","8":"2018-01-30 16:06:09","9":null,"10":null,"11":["ROLE_EMPLOYEE"],"12":"2018-01-30 16:06:09","13":"2018-01-30 16:06:09","14":null,"15":null,"16":null,"17":null,"18":null,"19":"u","20":null,"21":null,"22":null,"23":null,"24":null,"25":null,"26":null,"27":null,"28":null,"29":null,"30":null,"31":null,"32":null,"33":null,"34":null,"35":null,"36":null,"37":null,"38":null,"39":null,"40":0,"41":0,"42":0,"43":0,"44":0,"45":false,"46":"CN=gpotest,OU=Test,DC=example,DC=mu","47":null,"48":null,"49":null,"50":null,"51":null,"52":null} []
[2018-01-30 16:06:09] doctrine.DEBUG: "ROLLBACK" [] []
[2018-01-30 16:06:09] request.CRITICAL: Uncaught PHP Exception Doctrine\DBAL\Exception\NotNullConstraintViolationException: "An exception occurred while executing 'INSERT INTO fos_user_user (username, username_canonical, email, email_canonical, enabled, salt, password, last_login, confirmation_token, password_requested_at, roles, created_at, updated_at, date_of_birth, firstname, lastname, website, biography, gender, locale, timezone, phone, facebook_uid, facebook_name, facebook_data, twitter_uid, twitter_name, twitter_data, gplus_uid, gplus_name, gplus_data, token, two_step_code, ab_number, title, hire_date, employment_status, departure_date, departure_reason, local_balance, sick_balance, frozen_local_balance, carry_forward_local_balance, frozen_carry_forward_local_balance, is_no_probation_leaves, dn, created_by, updated_by, job_title_id, department_id, project_id, business_unit_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)' with params [null, null, null, null, 1, null, "", "2018-01-30 16:06:09", null, null, "a:1:{i:0;s:13:\"ROLE_EMPLOYEE\";}", "2018-01-30 16:06:09", "2018-01-30 16:06:09", null, null, null, null, null, "u", null, null, null, null, null, "null", null, null, "null", null, null, "null", null, null, null, null, null, null, null, null, 0, 0, 0, 0, 0, 0, "CN=gpotest,OU=Test,DC=example,DC=mu", null, null, null, null, null, null]: SQLSTATE[23000]: Integrity constraint violation: 1048 Column 'username' cannot be null" at E:\wamp64\www\acshrm\vendor\doctrine\dbal\lib\Doctrine\DBAL\Driver\AbstractMySQLDriver.php line 118 {"exception":"[object] (Doctrine\\DBAL\\Exception\\NotNullConstraintViolationException(code: 0): An exception occurred while executing 'INSERT INTO fos_user_user (username, username_canonical, email, email_canonical, enabled, salt, password, last_login, confirmation_token, password_requested_at, roles, created_at, updated_at, date_of_birth, firstname, lastname, website, biography, gender, locale, timezone, phone, facebook_uid, facebook_name, facebook_data, twitter_uid, twitter_name, twitter_data, gplus_uid, gplus_name, gplus_data, token, two_step_code, ab_number, title, hire_date, employment_status, departure_date, departure_reason, local_balance, sick_balance, frozen_local_balance, carry_forward_local_balance, frozen_carry_forward_local_balance, is_no_probation_leaves, dn, created_by, updated_by, job_title_id, department_id, project_id, business_unit_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)' with params [null, null, null, null, 1, null, \"\", \"2018-01-30 16:06:09\", null, null, \"a:1:{i:0;s:13:\\\"ROLE_EMPLOYEE\\\";}\", \"2018-01-30 16:06:09\", \"2018-01-30 16:06:09\", null, null, null, null, null, \"u\", null, null, null, null, null, \"null\", null, null, \"null\", null, null, \"null\", null, null, null, null, null, null, null, null, 0, 0, 0, 0, 0, 0, \"CN=gpotest,OU=Test,DC=example,DC=mu\", null, null, null, null, null, null]:\n\nSQLSTATE[23000]: Integrity constraint violation: 1048 Column 'username' cannot be null at E:\\wamp64\\www\\acshrm\\vendor\\doctrine\\dbal\\lib\\Doctrine\\DBAL\\Driver\\AbstractMySQLDriver.php:118, Doctrine\\DBAL\\Driver\\PDOException(code: 23000): SQLSTATE[23000]: Integrity constraint violation: 1048 Column 'username' cannot be null at E:\\wamp64\\www\\acshrm\\vendor\\doctrine\\dbal\\lib\\Doctrine\\DBAL\\Driver\\PDOStatement.php:107, PDOException(code: 23000): SQLSTATE[23000]: Integrity constraint violation: 1048 Column 'username' cannot be null at E:\\wamp64\\www\\acshrm\\vendor\\doctrine\\dbal\\lib\\Doctrine\\DBAL\\Driver\\PDOStatement.php:105)"} []
As you can see it says user found with the right username:
User gpotest found on LDAP {"action":"loadUserByUsername","username":"gpotest","result":"found"}
But then after binding:
security.INFO: User has been authenticated successfully. {"username":null}
However, by using the commented out Online LDAP server config in config.yml below, I was able to connect and authenticated successfully:
[2018-01-24 23:46:03] ldap_driver.DEBUG: ldap_bind(uid=gauss,dc=example,dc=com, ****) {"action":"ldap_bind","bind_rdn":"uid=gauss,dc=example,dc=com"} []
[2018-01-24 23:46:03] doctrine.DEBUG: SELECT t0.name AS name_1, t0.roles AS roles_2, t0.id AS id_3 FROM fos_user_group t0 INNER JOIN fos_user_user_group ON t0.id = fos_user_user_group.group_id WHERE fos_user_user_group.user_id = ? [188] []
[2018-01-24 23:46:03] security.INFO: User has been authenticated successfully. {"username":"gauss"} []
[2018-01-24 23:46:03] doctrine.DEBUG: "START TRANSACTION" [] []
[2018-01-24 23:46:03] doctrine.DEBUG: UPDATE fos_user_user SET last_login = ?, updated_at = ? WHERE id = ? ["2018-01-24 23:46:03","2018-01-24 23:46:03",188] []
[2018-01-24 23:46:03] doctrine.DEBUG: "COMMIT" [] []
[2018-01-24 23:46:03] security.DEBUG: Stored the security token in the session. {"key":"_security_user"} []
Here are my relevant file contents:
composer.json
"fr3d/ldap-bundle": "^3.0",
"friendsofsymfony/user-bundle": "^2.0",
"sonata-project/admin-bundle": "^3.23",
"sonata-project/doctrine-orm-admin-bundle": "^3.1",
"sonata-project/user-bundle": "^4.0",
"symfony/symfony": "3.4.*",
config.yml
fr3d_ldap:
# driver:
# host: ldap.forumsys.com
# user:
# baseDn: 'dc=example,dc=com'
# attributes:
# - { ldap_attr: uid, user_method: setUsername }
# - { ldap_attr: mail, user_method: setEmail }
# - { ldap_attr: cn, user_method: setFirstname }
# - { ldap_attr: sn, user_method: setLastname }
# filter: (&(ObjectClass=person))
driver:
host: example.mu
accountFilterFormat: (&('sAMAccountName'=%s))
username: gpotest
password: apassword
bindRequiresDn: false
user:
baseDn: 'dc=example,dc=mu'
attributes:
- { ldap_attr: sAMAccountName, user_method: setUsername }
- { ldap_attr: mail, user_method: setEmail }
# - { ldap_attr: cn, user_method: setFirstname }
- { ldap_attr: sn, user_method: setLastname }
filter: (&(ObjectClass=person))
usernameAttribute: 'sAMAccountName'
# Sonata User Bundle
sonata_user:
security_acl: false
manager_type: orm # can be orm or mongodb
class:
user: Application\Sonata\UserBundle\Entity\User
group: Application\Sonata\UserBundle\Entity\Group
admin: # Admin Classes
user:
class: LeavesOvertimeBundle\Admin\UserAdmin
controller: SonataAdminBundle:CRUD
translation: SonataUserBundle
fos_user:
db_driver: orm # can be orm or odm
firewall_name: main
user_class: Application\Sonata\UserBundle\Entity\User #Sonata\UserBundle\Entity\BaseUser
group:
group_class: Application\Sonata\UserBundle\Entity\Group #Sonata\UserBundle\Entity\BaseGroup
group_manager: sonata.user.orm.group_manager
service:
user_manager: sonata.user.orm.user_manager
from_email:
address: '%from_email%'
sender_name: AS
security.yml
security:
erase_credentials: false
# Sonata User Bundle
encoders:
FOS\UserBundle\Model\UserInterface: bcrypt
# acl:
# connection: default
role_hierarchy:
ROLE_USER:
- ROLE_SONATA_ADMIN
ROLE_EMPLOYEE:
- ROLE_USER
SONATA:
- ROLE_SONATA_PAGE_ADMIN_PAGE_EDIT # if you are using acl then this line must be commented
access_decision_manager:
strategy: unanimous
providers:
chain_provider:
chain:
providers: [fos_userbundle, fr3d_ldapbundle]
fr3d_ldapbundle:
id: fr3d_ldap.security.user.provider
fos_userbundle:
id: fos_user.user_provider.username
firewalls:
# Disabling the security for the web debug toolbar, the profiler and Assetic.
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
# -> custom firewall for the admin area of the URL
admin:
switch_user: true
pattern: /admin(.*)
fr3d_ldap: ~
context: user
form_login:
provider: fos_userbundle # - looking back at this code, this was probably an issue, but I remember trying combination of every provider I have above. In my final solution this must be removed so that it takes the chain provider
login_path: /admin/login
use_forward: false
check_path: /admin/login_check
failure_path: null
default_target_path: /admin/dashboard
always_use_default_target_path: true
logout:
path: /admin/logout
target: /admin/login
anonymous: true
main:
switch_user: true
fr3d_ldap: ~
pattern: .*
context: user
form_login:
provider: fos_userbundle
login_path: /login
use_forward: false
check_path: /login_check
failure_path: null
logout: true
anonymous: true
User class: (I have removed the irrelevent fields)
namespace Application\Sonata\UserBundle\Entity;
use Sonata\UserBundle\Entity\BaseUser as BaseUser;
use FR3D\LdapBundle\Model\LdapUserInterface;
class User extends BaseUser implements LdapUserInterface
{
/**
* @var int $id
*/
protected $id;
/**
* Get id
*
* @return int $id
*/
public function getId()
{
return $this->id;
}
/**
* @var string|null
*/
protected $dn;
public function __construct()
{
parent::__construct();
if (empty($this->roles)) {
$this->roles[] = 'ROLE_EMPLOYEE';
}
}
/**
* @param mixed $dn
*
* @return User
*/
public function setDn($dn) {
$this->dn = $dn;
return $this;
}
/**
* @return mixed
*/
public function getDn() {
return $this->dn;
}
Extended classes above basically lead to FOSUserBundle's user class for username property https://github.com/sonata-project/SonataUserBundle/blob/4.x/src/Entity/BaseUser.php https://github.com/sonata-project/SonataUserBundle/blob/4.x/src/Model/User.php https://github.com/FriendsOfSymfony/FOSUserBundle/blob/master/Model/User.php
Overridden user.orm.xml: (I have removed the irrelevent fields)
<?xml version="1.0" encoding="UTF-8"?>
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping
http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
<entity name="Application\Sonata\UserBundle\Entity\User" table="fos_user_user" repository-class="MyBundle\Repository\UserRepository">
<id name="id" column="id" type="integer">
<generator strategy="AUTO" />
</id>
<field name="dn" column="dn" type="string" length="255" nullable="true" />
</entity>
</doctrine-mapping>
Parent definition: https://github.com/sonata-project/SonataUserBundle/blob/4.x/src/Resources/config/doctrine/BaseUser.orm.xml
Update 1: Still couldn't make it work so we tried to put 'mail' in place of samaccountname and, I don't know why if I was in the middle of debugging that affected my token, but it worked. Email was working as a username. Logged out and tried it with another one and it worked. Strangely after me trying to add a parameter to be saved, mail as username stopped working. This time when I made another person test, his email didn't work but his samaccountname (the thing I was trying to make work all this time) worked... At this point I have no idea what the hell is going on anymore.
Update 2 Here is the log using mail instead of sAMAccountName for setUsername method (after that it inserts in DB correctly). Another weird thing is givenName attr that contains firstName doesn't return anything, but using a AD explorer from MS, I can see that it has the firstName.:
[2018-02-05 11:42:51] ldap_driver.DEBUG: ldap_search(dc=example,dc=mu, (&(sAMAccountName=leaves)), [array]) {"action":"ldap_search","base_dn":"dc=example,dc=mu","filter":"(&(sAMAccountName=leaves))","attributes":[]} []
[2018-02-05 11:42:51] security.INFO: User leaves found on LDAP {"action":"loadUserByUsername","username":"leaves","result":"found"} []
[2018-02-05 11:42:51] ldap_driver.DEBUG: ldap_bind(CN=Leaves_Test HRM,OU=06 IT,DC=example,DC=mu, ****) {"action":"ldap_bind","bind_rdn":"CN=Leaves_Test HRM,OU=06 IT,DC=example,DC=mu"} []
[2018-02-05 11:42:51] security.INFO: User has been authenticated successfully. {"username":"Leaves_Test.HRM@email.com"} []
And config.yml:
driver:
host: example.mu
# accountDomainName: example
accountDomainNameShort: example
# accountFilterFormat: (&('sAMAccountName'=%s))
username: leaves
password: P@55w0rd
user:
baseDn: '%base_dn%'
attributes:
- { ldap_attr: mail, user_method: setUsername }
- { ldap_attr: mail, user_method: setEmail }
- { ldap_attr: cn, user_method: setFirstname }
- { ldap_attr: sn, user_method: setLastname }
# filter: (&(ObjectClass=person))
usernameAttribute: 'sAMAccountName'
All in all, I get the feeling that at the moment of retrieving information, I am only able to pull mail, cn, sn attributes. Also, see at the first line of the log at the end of the line, there is attributes":[]. Is it normal to be empty?
My problem was I was only getting like email, sn, cn information after binding with MS Active Directory, not even samaccountname was stored in my User object.
The solution is to use the bundle's custom hydrator:
https://github.com/Maks3w/FR3DLdapBundle/blob/master/Resources/doc/cookbook/custom_hydrator.md. Through this you will be able to get access to every attributes in the LDAP record you searched for. If you follow the documentation it is all you need to fix the said issue.
However, in my version I import existing users via a CSV importer that includes their Usernames from LDAP but I cannot generate their DN string automatically, thus cannot login with only their Usernames, as their DN string contains specific "OU" information.
I solved this issue by loading the user from LDAP on every login to get up-to-date information like DN string and others that change frequently. I then either update an existing user or create a new one depending on repository search result, which also allows imported users to login and no duplicate entries of users. Thanks to ejkun for the elegant solution: https://github.com/Maks3w/FR3DLdapBundle/issues/53#issuecomment-330905167. There's also a bonus on how to detect disabled accounts in AD.
Here is my version of UserHydrator:
use Doctrine\ORM\EntityManager;
use FR3D\LdapBundle\Hydrator\HydratorInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Application\Sonata\UserBundle\Entity\User;
class UserHydrator implements HydratorInterface
{
protected $entityManager;
public function __construct(EntityManager $entityManager)
{
$this->entityManager = $entityManager;
}
/**
* Populate a user with the data retrieved from LDAP.
*
* @param array $ldapEntry LDAP result information as a multi-dimensional array.
* see {@link http://www.php.net/function.ldap-get-entries} for array format examples.
*
* @return UserInterface
*/
public function hydrate(array $ldapEntry)
{
if (!$ldapEntry) {
return null;
}
if (!array_key_exists('samaccountname', $ldapEntry)) {
return null;
}
$username = $ldapEntry['samaccountname'][0];
$userFromDB = $this->entityManager->getRepository('ApplicationSonataUserBundle:User')->findOneBy(['username' => $username]);
if ($userFromDB == null) {
$user = new User();
$user->setPassword('');
}
else {
$user = $userFromDB;
}
// These are basically if statements without else in a short form, but do not call set methods with null
array_key_exists('mail', $ldapEntry) ? $user->setEmail($ldapEntry['mail'][0]) : null;
array_key_exists('givenname', $ldapEntry) ? $user->setFirstName($ldapEntry['givenname'][0]) : null;
array_key_exists('sn', $ldapEntry) ? $user->setLastName($ldapEntry['sn'][0]) : null;
array_key_exists('distinguishedname', $ldapEntry) ? $user->setDn($ldapEntry['distinguishedname'][0]) : null;
/**
* 512 = Enabled
* 514 = Disabled
* 66048 = Enabled, password never expires
* 66050 = Disabled, password never expires
*/
$user->setEnabled(array_key_exists('useraccountcontrol', $ldapEntry) ?
$ldapEntry['useraccountcontrol'][0] == 512 || 66048 ? true : false : true);
$user->setUsername($username);
return $user;
}
}
Security.yml, reverse the chain provider order:
providers:
chain_provider:
chain:
providers: [fr3d_ldapbundle, fos_userbundle]
fr3d_ldapbundle:
id: fr3d_ldap.security.user.provider
fos_userbundle:
id: fos_user.user_provider.username