diff --git a/docs/install/configuration-options.md b/docs/install/configuration-options.md index ba775e1c..81c9448a 100755 --- a/docs/install/configuration-options.md +++ b/docs/install/configuration-options.md @@ -14,6 +14,32 @@ Number of expired storing records `session history`, values: - `false` Store all records without deleting - `integer` Count of records for storing +#### searchUsersInLdap (Type: `boolean, integer`, Default value: `false`) + +If this option is `true`, it will be possible to search users in LDAP. + +To use this option, you need to install [yetopen/yii2-usuario-ldap](https://github.com/YetOpen/yii2-usuario-ldap): + +```shell +composer require yetopen/yii2-usuario-ldap "*" +``` + +#### ldapUserAttributes (Type: `array`) + +This array maps the user attributes to sync from LDAP + +Default to: +```php +[ + 'email' => 'mail', + 'username' => 'samaccountname', +] +``` + +#### ldapProfileAttributes (Type: `array`) + +This array maps the profile attributes to sync from LDAP + #### timeoutSessionHistory (Type: `boolean, integer`, Default value: `false`) How long store `session history` after expiring, values: diff --git a/src/User/Bootstrap.php b/src/User/Bootstrap.php index 1b92e446..b8c19411 100755 --- a/src/User/Bootstrap.php +++ b/src/User/Bootstrap.php @@ -154,6 +154,12 @@ function () use ($model) { $di->set(Search\RoleSearch::class); } + if (Yii::$app->getModule('user')->searchUsersInLdap) { + if (!class_exists('kartik\typeahead\Typeahead')) { + throw new InvalidConfigException('The kartik-v/yii2-widget-typeahead library must be installed when searchUsersInLdap is true.'); + } + } + // Attach an event to check if the password has expired if (null !== Yii::$app->getModule('user')->maxPasswordAge) { YiiEvent::on(SecurityController::class, FormEvent::EVENT_AFTER_LOGIN, function (FormEvent $event) { diff --git a/src/User/Controller/AdminController.php b/src/User/Controller/AdminController.php index 05b1ca2b..e99e60bf 100755 --- a/src/User/Controller/AdminController.php +++ b/src/User/Controller/AdminController.php @@ -143,7 +143,11 @@ public function actionCreate() $this->make(AjaxRequestModelValidator::class, [$user])->validate(); - if ($user->load(Yii::$app->request->post()) && $user->validate()) { + if ($user->load(Yii::$app->request->post())) { + if (!$user->validate()) { + Yii::$app->session->setFlash('danger', implode(', ', $user->getErrorSummary(false))); + return $this->render('create', ['user' => $user]); + } $this->trigger(UserEvent::EVENT_BEFORE_CREATE, $event); $mailService = MailFactory::makeWelcomeMailerService($user); diff --git a/src/User/Dictionary/UserSourceType.php b/src/User/Dictionary/UserSourceType.php new file mode 100644 index 00000000..4e94d1cc --- /dev/null +++ b/src/User/Dictionary/UserSourceType.php @@ -0,0 +1,37 @@ + \Yii::t('usuario', 'Local'), + static::LDAP => \Yii::t('usuario', 'LDAP'), + ]; + } + + /** + * Returns the dictionary value for the given code + * @param $key + * @return string|null + * @throws \Exception + */ + public static function get($key) + { + return ArrayHelper::getValue(static::all(), $key); + } + + +} diff --git a/src/User/Model/User.php b/src/User/Model/User.php index 1e60d71b..d3ea817a 100644 --- a/src/User/Model/User.php +++ b/src/User/Model/User.php @@ -11,10 +11,14 @@ namespace Da\User\Model; +use Da\User\Dictionary\UserSourceType; use Da\User\Helper\SecurityHelper; use Da\User\Query\UserQuery; +use Da\User\Service\InitLdapUserService; use Da\User\Traits\ContainerAwareTrait; use Da\User\Traits\ModuleAwareTrait; +use lhs\Yii2SaveRelationsBehavior\SaveRelationsBehavior; +use yetopen\usuarioLdap\UsuarioLdapComponent; use Yii; use yii\base\Exception; use yii\base\InvalidConfigException; @@ -57,6 +61,7 @@ * @property string $last_login_ip * @property int $password_changed_at * @property int $password_age + * @property int $ldap_uid * Defined relations: * @property SocialNetworkAccount[] $socialNetworkAccounts * @property Profile $profile @@ -70,14 +75,27 @@ class User extends ActiveRecord implements IdentityInterface public const OLD_EMAIL_CONFIRMED = 0b01; public const NEW_EMAIL_CONFIRMED = 0b10; + // ldap error + public const LDAP_INVALID_USER = -1; + /** * @var string Plain password. Used for model validation */ public $password; + + /** + * @var string Stores LDAP uid of the user during creation. + */ + public $ldapUid; + /** * @var array connected account list */ protected $connectedAccounts; + /** + * @var \Adldap\Models\User|null + */ + protected $ldapUser; /** * {@inheritdoc} @@ -127,6 +145,26 @@ public static function findIdentityByAccessToken($token, $type = null) throw new NotSupportedException('Method "' . __CLASS__ . '::' . __METHOD__ . '" is not implemented.'); } + /** + * {@inheritdoc} + */ + public function beforeValidate() + { + if ($this->module->searchUsersInLdap && $this->source == UserSourceType::LDAP && $this->isNewRecord) { + if ($this->ldapUid == self::LDAP_INVALID_USER) { + $this->addError('ldapUid', Yii::t('usuario', 'Invalid LDAP user')); + return false; + } + /** @var UsuarioLdapComponent $ldapComponent */ + $ldapComponent = Yii::$app->usuarioLdap; + $this->ldapUser = $ldapComponent->findLdapUser($this->ldapUid); + if ($this->ldapUser !== null) { + $this->make(InitLdapUserService::class, [$this, $this->module->ldapUserAttributes, $this->ldapUser])->run(); + } + } + return parent::beforeValidate(); + } + /** * {@inheritdoc} */ @@ -163,6 +201,9 @@ public function afterSave($insert, $changedAttributes) if ($insert && $this->profile === null) { $profile = $this->make(Profile::class); + if ($this->ldapUser !== null) { + $this->make(InitLdapUserService::class, [$profile, $this->module->ldapProfileAttributes, $this->ldapUser])->run(); + } $profile->link('user', $this); } } @@ -184,6 +225,13 @@ public function behaviors() ]; } + $behaviors['saveRelations'] = [ + 'class' => SaveRelationsBehavior::class, + 'relations' => [ + 'profile' => ['cascadeDelete' => true] + ] + ]; + return $behaviors; } @@ -204,6 +252,7 @@ public function attributeLabels() 'last_login_ip' => Yii::t('usuario', 'Last login IP'), 'password_changed_at' => Yii::t('usuario', 'Last password change'), 'password_age' => Yii::t('usuario', 'Password age'), + 'ldapUid' => Yii::t('usuario', 'Search'), ]; } @@ -263,6 +312,10 @@ public function rules() 'twoFactorEnabledNumber' => ['auth_tf_enabled', 'boolean'], 'twoFactorTypeLength' => ['auth_tf_type', 'string', 'max' => 20], 'twoFactorMobilePhoneLength' => ['auth_tf_mobile_phone', 'string', 'max' => 20], + + // ldapUid rules + 'ldapUid' => ['ldapUid', 'string'], + 'ldapUidRequired' => ['ldapUid', 'required', 'on' => $this->module->searchUsersInLdap], ]; } diff --git a/src/User/Module.php b/src/User/Module.php index d8b4e03d..7dd7a62a 100755 --- a/src/User/Module.php +++ b/src/User/Module.php @@ -32,6 +32,21 @@ class Module extends BaseModule * if equals false records will not be deleted */ public $numberSessionHistory = false; + /** + * @var bool If this option is `true`, it will be possible to search users in LDAP + */ + public $searchUsersInLdap = false; + /** + * @var array user attributes to sync from ldap + */ + public $ldapUserAttributes = [ + 'email' => 'mail', + 'username' => 'samaccountname', + ]; + /** + * @var array profile attributes to sync from ldap + */ + public $ldapProfileAttributes = []; /** * @var int|bool The time after which the expired 'session history' will be deleted * if equals false records will not be deleted diff --git a/src/User/Service/InitLdapUserService.php b/src/User/Service/InitLdapUserService.php new file mode 100644 index 00000000..f64dbfa5 --- /dev/null +++ b/src/User/Service/InitLdapUserService.php @@ -0,0 +1,43 @@ +model = $model; + $this->attributes = $attributes; + $this->ldapUser = $ldapUser; + } + + /** + * @inheritDoc + */ + public function run() + { + foreach ($this->attributes as $attribute => $ldapAttribute) { + // if Closure call and assign + if ($ldapAttribute instanceof \Closure) { + $this->model->$attribute = $ldapAttribute($this->ldapUser, $attribute); + continue; + } + $value = $this->ldapUser->$ldapAttribute; + if (empty($value)) { + continue; + } + $this->model->$attribute = ArrayHelper::getValue($value, 0); + } + } +} diff --git a/src/User/resources/i18n/it/usuario.php b/src/User/resources/i18n/it/usuario.php index 668cf106..6cc82f38 100644 --- a/src/User/resources/i18n/it/usuario.php +++ b/src/User/resources/i18n/it/usuario.php @@ -1,5 +1,4 @@ 'Dettagli account', 'Account details have been updated' => 'I dettagli del tuo account sono stati aggiornati', 'Account settings' => 'Impostazioni account', + 'Active' => 'Attivo', 'Already registered? Sign in!' => 'Già registrato? Accedi!', 'An email with instructions to create a new password has been sent to {email} if it is associated with an {appName} account. Your existing password has not been changed.' => 'Una mail con le istruzioni per creare una nuova password è stata inviata all\'indirizzo {email} se associato a un account {appName}. La tua password non è ancora stata cambiata.', 'An error occurred processing your request' => 'Si è verificato un errore durante l\'elaborazione della richiesta', + 'Application not configured for two factor authentication.' => 'Autenticazione a due fattori (2FA) non abilitata per l\'applicazione', 'Are you sure you want to block this user?' => 'Sicuro di voler bloccare questo utente?', 'Are you sure you want to confirm this user?' => 'Sicuro di voler confermare questo utente?', 'Are you sure you want to delete this user?' => 'Sicuro di voler eliminare questo utente?', @@ -53,12 +54,12 @@ 'Authorization rule has been updated.' => 'Regola di autorizzazione modificata.', 'Awesome, almost there. Now you need to click the confirmation link sent to your new email address.' => 'Fantastico, ci siamo quasi. Ora devi solo visitare il collegamento di conferma che è stato inviato al tuo nuovo indirizzo email.', 'Awesome, almost there. Now you need to click the confirmation link sent to your old email address.' => 'Fantastico, ci siamo quasi. Ora devi solo visitare il collegamento di conferma che è stato inviato al tuo vecchio indirizzo email.', - 'Can\'t scan? Copy the code instead.' => 'Non puoi scansionare? Copia il codice.', 'Back to privacy settings' => 'Torna alle impostazioni di privacy', 'Bio' => 'Bio', 'Block' => 'Blocca', 'Block status' => 'Stato di blocco', 'Blocked at {0, date, MMMM dd, YYYY HH:mm}' => 'Bloccato il {0, date, dd MMMM YYYY HH:mm}', + 'Can\'t scan? Copy the code instead.' => 'Non puoi scansionare? Copia il codice.', 'Cancel' => 'Annulla', 'Cannot assign role "{0}" as the AuthManager is not configured on your console application.' => 'Impossibile assegnare il ruolo "{0}" perché l\'AuthManager non è configurato nella applicazione da console.', 'Change your avatar at Gravatar.com' => 'Modifica la tua immagine di profilo su Gravatar.com', @@ -84,6 +85,7 @@ 'Create new rule' => 'Crea nuova regola', 'Created at' => 'Creata il', 'Credentials will be sent to the user by email' => 'Le credenziali verranno inviate all\'utente via email', + 'Current' => 'Attuale', 'Current password' => 'Password attuale', 'Current password is not valid' => 'La password attuale non è valida', 'Data privacy' => 'Data privacy', @@ -118,13 +120,16 @@ 'Hello' => 'Ciao', 'Here you can download your personal data in a comma separated values format.' => 'Da qui puoi scaricare i tuoi dati in formato CSV.', 'I agree processing of my personal data and the use of cookies to facilitate the operation of this site. For more information read our {privacyPolicy}' => 'Consento al trattamento dei miei dati personali e all\'uso dei cookie per agevolare le attività di questo sito. Per ulteriori informazioni leggere la nostra {privacyPolicy}', + 'IP' => 'IP', 'If you already registered, sign in and connect this account on settings page' => 'Se sei già registrato accedi e collega questo account nella pagina delle impostazioni', 'If you cannot click the link, please try pasting the text into your browser' => 'Se non puoi fare click sul link prova a copiare ed incollare il testo nel browser', 'If you did not make this request you can ignore this email' => 'Se non hai effettuato tu la richiesta puoi ignorare questa email', + 'If you haven\'t received a password, you can reset it at' => 'Se non hai ricevuto una password, puoi reimpostarla su', 'Impersonate this user' => 'Impersona questo utente', 'In order to complete your registration, please click the link below' => 'Per completare la registrazione fai click sul collegamento qui sotto', 'In order to complete your request, please click the link below' => 'Per completare la richiesta fai click sul collegamento qui sotto', 'In order to finish your registration, we need you to enter following fields' => 'Per finalizzare la registrazione devi fornire le seguenti informazioni', + 'Inactive' => 'Inattivo', 'Information' => 'Informazioni', 'Insert' => 'Inserisci', 'Insert the code you received by SMS.' => 'Inserisci il codice ricevuto tramite SMS.', @@ -138,6 +143,7 @@ 'It will be deleted forever' => 'Sarà eliminato per sempre', 'Items' => 'Elementi', 'Joined on {0, date}' => 'Registrato il {0, date}', + 'Last activity' => 'Ultima attività', 'Last login IP' => 'IP ultimo accesso', 'Last login time' => 'Data ultimo accesso', 'Last password change' => 'Data cambio password', @@ -200,11 +206,15 @@ 'Scan the QrCode with Google Authenticator App, then insert its temporary code on the box and submit.' => 'Scansiona il codice QR con l\'applicazione Google Authenticator, poi inserisci il codice temporaneo nel riquadro ed invia.', 'Select rule...' => 'Seleziona una regola...', 'Send password recovery email' => 'Invia email di recupero password', + 'Session ID' => 'ID sessione', + 'Session history' => 'Cronologia sessioni', 'Sign in' => 'Accedi', 'Sign up' => 'Registrati', 'Something went wrong' => 'È successo qualcosa di strano', + 'Status' => 'Stato', 'Submit' => 'Invia', 'Switch identities is disabled.' => 'Il cambio identità è disabilitato', + 'Terminate all sessions' => 'Termina tutte le sessioni', 'Text message' => 'Messaggio di testo tramite SMS', 'Thank you for signing up on {0}' => 'Grazie per esserti registrato su {0}', 'Thank you, registration is now complete.' => 'Grazie, la tua registrazione è completa.', @@ -213,12 +223,14 @@ 'The email address set is: "{0}".' => 'L\'indirizzo email impostato è: "{0}".', 'The email sending failed, please check your configuration.' => 'L\'invio della email non è riuscito, verifica la configurazione', 'The phone number set is: "{0}".' => 'Il numero di telefono impostato è: "{0}".', + 'The requested page does not exist.' => 'La pagina richiesta non esiste.', 'The sms sending failed, please check your configuration.' => 'L\'invio del messaggio di testo non è riuscito, verifica il numero di cellulare o contatta l\'assistenza', 'The verification code is incorrect.' => 'Il codice di verifica non è corretto.', 'There is neither role nor permission with name "{0}"' => 'Non esiste un ruolo o permesso di nome "{0}', 'There was an error in saving user' => 'Errore in salvataggio utente', 'This account has already been connected to another user' => 'Questo account è già stato associato ad un altro utente', 'This email address has already been taken' => 'Questo indirizzo email è già stato registrato', + 'This is the code to insert to enable two factor authentication' => 'Questo è il codice da inserire per abilitare l\'autenticazione a due fattori', 'This username has already been taken' => 'Questo nome utente è già stato registrato', 'This will disable two factor authentication. Are you sure?' => 'Stai per disabilitare l\'autenticazione a due fattori. Sei sicuro?', 'This will remove your personal data from this site. You will no longer be able to sign in.' => 'Questa operazione rimuoverà i tuoi dati personali da questo sito. Non ti sarà più possibile effettuare l\'accesso.', @@ -253,9 +265,12 @@ 'Update rule' => 'Modifica regola', 'Update user account' => 'Modifica account utente', 'Updated at' => 'Aggiornata il', + 'User ID' => 'ID utente', 'User account could not be created.' => 'Impossibile creare il nuovo utente.', + 'User agent' => 'User agent', 'User block status has been updated.' => 'Stato di blocco aggiornato.', 'User could not be registered.' => 'Impossibile registrare l\'utente.', + 'User does not have sufficient permissions.' => 'L\'utente non ha i permessi per accedere.', 'User has been confirmed' => 'L\'utente è stato confermato', 'User has been created' => 'L\'utente è stato creato', 'User has been deleted' => 'L\'utente è stato eliminato', @@ -277,8 +292,11 @@ 'You are about to delete all your personal data from this site.' => 'Stai per eliminare tutti i tuoi dati personali da questo sito.', 'You can assign multiple roles or permissions to user by using the form below' => 'Puoi assegnare più permessi o ruoli all\'utente usando il form sotto', 'You can connect multiple accounts to be able to log in using them' => 'Puoi collegare account esterni e fare login con quelli', + 'You cannot block your own account.' => 'Non puoi bloccare il tuo stesso account.', 'You cannot remove your own account' => 'Non puoi eliminare il tuo account', + 'You cannot remove your own account.' => 'Non puoi eliminare il tuo stesso account.', 'You need to confirm your email address' => 'Devi confermare il tuo indirizzo email', + 'You received this email because someone, possibly you or someone on your behalf, have created an account at {app_name}' => 'Hai ricevuto questa email perché qualcuno, presumibilmente tu, ha creato un account su {app_name}', 'Your account details have been updated' => 'I dettagli del tuo account sono stati aggiornati', 'Your account has been blocked' => 'Il tuo account è stato bloccato', 'Your account has been blocked.' => 'Il tuo account è stato bloccato.', @@ -300,23 +318,7 @@ '{0, date, MMM dd, YYYY HH:mm}' => '{0, date, MMM dd, YYYY HH:mm}', '{0, date, MMMM dd, YYYY HH:mm}' => '{0, date, dd MMMM YYYY HH:mm}', '{0} cannot be blank.' => '{0} non può essere vuoto.', - 'Active' => 'Attivo', - 'Application not configured for two factor authentication.' => 'Autenticazione a due fattori (2FA) non abilitata per l\'applicazione', - 'Current' => 'Attuale', - 'IP' => 'IP', - 'If you haven\'t received a password, you can reset it at' => 'Se non hai ricevuto una password, puoi reimpostarla su', - 'Inactive' => 'Inattivo', - 'Last activity' => 'Ultima attività', - 'Session ID' => 'ID sessione', - 'Session history' => 'Cronologia sessioni', - 'Status' => 'Stato', - 'Terminate all sessions' => 'Termina tutte le sessioni', - 'The requested page does not exist.' => 'La pagina richiesta non esiste.', - 'This is the code to insert to enable two factor authentication' => 'Questo è il codice da inserire per abilitare l\'autenticazione a due fattori', - 'User ID' => 'ID utente', - 'User agent' => 'User agent', - 'User does not have sufficient permissions.' => 'L\'utente non ha i permessi per accedere.', - 'You cannot block your own account.' => 'Non puoi bloccare il tuo stesso account.', - 'You cannot remove your own account.' => 'Non puoi eliminare il tuo stesso account.', - 'You received this email because someone, possibly you or someone on your behalf, have created an account at {app_name}' => 'Hai ricevuto questa email perché qualcuno, presumibilmente tu, ha creato un account su {app_name}', + 'Filter as you type...' => 'Digita per filtrare...', + 'Invalid LDAP user' => 'Utente LDAP non valido', + 'Search' => 'Cerca', ]; diff --git a/src/User/resources/views/admin/_user.php b/src/User/resources/views/admin/_user.php index 5e7303e5..f9b35586 100644 --- a/src/User/resources/views/admin/_user.php +++ b/src/User/resources/views/admin/_user.php @@ -13,8 +13,57 @@ * @var yii\widgets\ActiveForm $form * @var \Da\User\Model\User $user */ -?> -field($user, 'email')->textInput(['maxlength' => 255]) ?> -field($user, 'username')->textInput(['maxlength' => 255]) ?> -field($user, 'password')->passwordInput() ?> +use Da\User\Dictionary\UserSourceType; +use dosamigos\selectize\SelectizeTextInput; +use yii\helpers\Html; +use yii\helpers\Url; + +$source = Yii::$app->request->get('source') ?: $user->source; +$ldapUidId = Html::getInputId($user, 'ldapUid'); +$sourceId = Html::getInputId($user, 'source'); +$this->registerJs(<<isNewRecord) { + if (Yii::$app->getModule('user')->searchUsersInLdap && $source == UserSourceType::LDAP) { + echo $form->field($user, 'source')->dropDownList(UserSourceType::all(), ['value' => $source]); + echo $form->field($user, 'ldapUid')->widget(SelectizeTextInput::class, [ + 'loadUrl' => Url::to(['/usuario-ldap/ldap/search']), + 'queryParam' => 'q', + 'options' => [ + 'placeholder' => Yii::t('usuario', 'Filter as you type...'), + 'autocomplete' => 'off', + ], + 'clientOptions' => [ + 'valueField' => 'value', + 'labelField' => 'label', + 'searchField' => ['value', 'label', 'q'], + 'create' => false, + 'maxItems' => 1, + 'onChange' => new \yii\web\JsExpression(" + function(value) { + console.log(value); + updateFromLdap(value); + } + "), + ], + ]); + } else { + echo $form->field($user, 'source')->dropDownList(UserSourceType::all(), ['value' => $source]); + echo $form->field($user, 'email')->textInput(['maxlength' => 255]); + echo $form->field($user, 'username')->textInput(['maxlength' => 255]); + echo $form->field($user, 'password')->passwordInput(); + } +} else { + echo $form->field($user, 'email')->textInput(['maxlength' => 255]); + echo $form->field($user, 'username')->textInput(['maxlength' => 255]); + echo $form->field($user, 'password')->passwordInput(); +} diff --git a/src/User/resources/views/admin/create.php b/src/User/resources/views/admin/create.php index f65d7de1..ee20fd71 100644 --- a/src/User/resources/views/admin/create.php +++ b/src/User/resources/views/admin/create.php @@ -12,6 +12,7 @@ use yii\bootstrap\ActiveForm; use yii\bootstrap\Nav; use yii\helpers\Html; +use yii\widgets\Pjax; /** * @var yii\web\View $this @@ -83,7 +84,9 @@ 'A password will be generated automatically if not provided' ) ?>. - 'pjax-user-create']); + $form = ActiveForm::begin( [ 'layout' => 'horizontal', 'enableAjaxValidation' => true, @@ -107,7 +110,8 @@ - +