Skip to content
This repository has been archived by the owner on Dec 21, 2022. It is now read-only.

Commit

Permalink
API Support "update LDAP from local" feature for LDAP managed users.
Browse files Browse the repository at this point in the history
  • Loading branch information
Sean Harvey committed Jun 22, 2016
1 parent 5670a0a commit e4b5656
Show file tree
Hide file tree
Showing 11 changed files with 470 additions and 63 deletions.
32 changes: 13 additions & 19 deletions .travis.yml
Expand Up @@ -4,32 +4,26 @@ language: php

sudo: false

php:
- 5.4

env:
global:
- CORE_RELEASE=3.1
matrix:
- DB=MYSQL

matrix:
include:
- php: 5.3
env: DB=MYSQL
env: DB=MYSQL CORE_RELEASE=3.1
- php: 5.4
env: DB=MYSQL CORE_RELEASE=3.2
- php: 5.5
env: DB=MYSQL CORE_RELEASE=3.3
- php: 5.6
env: DB=MYSQL CORE_RELEASE=3.4

before_install:
- echo "extension=ldap.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini
- echo "extension=ldap.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini

before_script:
- composer self-update || true
- phpenv rehash
- git clone git://github.com/silverstripe-labs/silverstripe-travis-support.git ~/travis-support
- php ~/travis-support/travis_setup.php --source `pwd` --target ~/builds/ss
- cd ~/builds/ss
- git config --global user.email "travis@example.com"
- git config --global user.name "Travis test-runner"
- composer self-update || true
- phpenv rehash
- git clone git://github.com/silverstripe-labs/silverstripe-travis-support.git ~/travis-support
- php ~/travis-support/travis_setup.php --source `pwd` --target ~/builds/ss
- cd ~/builds/ss

script:
- vendor/bin/phpunit activedirectory/tests/

8 changes: 8 additions & 0 deletions CHANGELOG.md
@@ -1,5 +1,13 @@
# Changelog

## 3.0.0 (Unreleased)

Adding features to allow writing Member data back to LDAP.
See [Writing LDAP data from SilverStripe](docs/en/developer.md#writing-ldap-data-from-silverstripe).

- IsImportedFromLDAP field removed from Group and Member. To determine if
either of these are imported from LDAP, check if the GUID field is not NULL.

## 2.0.0

LDAP functionality that modifies user data (i.e. password reset) will require
Expand Down
7 changes: 1 addition & 6 deletions code/extensions/LDAPGroupExtension.php
Expand Up @@ -13,7 +13,6 @@ class LDAPGroupExtension extends DataExtension
// Unique user identifier, same field is used by SAMLMemberExtension
'GUID' => 'Varchar(50)',
'DN' => 'Text',
'IsImportedFromLDAP' => 'Boolean',
'LastSynced' => 'SS_Datetime'
);

Expand Down Expand Up @@ -43,18 +42,14 @@ class LDAPGroupExtension extends DataExtension
public function updateCMSFields(FieldList $fields)
{
// Add read-only LDAP metadata fields.
$fields->replaceField('IsImportedFromLDAP', $readOnly[] = new ReadonlyField(
'IsImportedFromLDAP',
_t('LDAPGroupExtension.ISIMPORTEDFROMLDAP', 'Is group imported from LDAP/AD?')
));
$fields->addFieldToTab('Root.LDAP', new ReadonlyField('GUID'));
$fields->addFieldToTab('Root.LDAP', new ReadonlyField('DN'));
$fields->addFieldToTab('Root.LDAP', new ReadonlyField(
'LastSynced',
_t('LDAPGroupExtension.LASTSYNCED', 'Last synced'))
);

if ($this->owner->IsImportedFromLDAP) {
if ($this->owner->GUID) {
$fields->replaceField('Title', new ReadonlyField('Title'));
$fields->replaceField('Description', new ReadonlyField('Description'));
// Surface the code which is normally hidden from the CMS user.
Expand Down
153 changes: 129 additions & 24 deletions code/extensions/LDAPMemberExtension.php
@@ -1,6 +1,6 @@
<?php
/**
* Class LDAPMemberExtension
* Class LDAPMemberExtension.
*
* Adds mappings from AD attributes to SilverStripe {@link Member} fields.
*/
Expand All @@ -12,9 +12,9 @@ class LDAPMemberExtension extends DataExtension
private static $db = array(
// Unique user identifier, same field is used by SAMLMemberExtension
'GUID' => 'Varchar(50)',
'IsImportedFromLDAP' => 'Boolean',
'Username' => 'Varchar(64)',
'IsExpired' => 'Boolean',
'LastSynced' => 'SS_Datetime'
'LastSynced' => 'SS_Datetime',
);

/**
Expand All @@ -26,23 +26,56 @@ class LDAPMemberExtension extends DataExtension
*/
private static $ldap_field_mappings = array(
'givenname' => 'FirstName',
'samaccountname' => 'Username',
'sn' => 'Surname',
'mail' => 'Email'
'mail' => 'Email',
);

/**
* The location (relative to /assets) where to save thumbnailphoto data.
*
* @var string
* @config
*/
private static $ldap_thumbnail_path = 'Uploads';

/**
* @var array
* When enabled, LDAP managed Member records (GUID flag)
* have their data written back to LDAP on write.
*
* This requires setting write permissions on the user configured in the LDAP
* credentials, which is why this is disabled by default.
*
* @var bool
* @config
*/
private static $dependencies = array(
'ldapService' => '%$LDAPService'
);
private static $update_ldap_from_local = false;

/**
* If enabled, Member records with a Username field have the user created in LDAP
* on write.
*
* This requires setting write permissions on the user configured in the LDAP
* credentials, which is why this is disabled by default.
*
* Note that some constants must be configured in your environment file:
* LDAP_NEW_USERS_DN - where to place users in the directory. e.g. "OU=Users,DC=mydomain,DC=com"
*
* @var bool
* @config
*/
private static $create_users_in_ldap = false;

/**
* If enabled, deleting Member records mapped to LDAP deletes the LDAP user.
*
* This requires setting write permissions on the user configured in the LDAP
* credentials, which is why this is disabled by default.
*
* @var bool
* @config
*/
private static $delete_users_in_ldap = false;

/**
* @param FieldList $fields
Expand All @@ -51,10 +84,6 @@ public function updateCMSFields(FieldList $fields)
{
// Redo LDAP metadata fields as read-only and move to LDAP tab.
$ldapMetadata = array();
$fields->replaceField('IsImportedFromLDAP', $ldapMetadata[] = new ReadonlyField(
'IsImportedFromLDAP',
_t('LDAPMemberExtension.ISIMPORTEDFROMLDAP', 'Is user imported from LDAP/AD?')
));
$fields->replaceField('GUID', $ldapMetadata[] = new ReadonlyField('GUID'));
$fields->replaceField('IsExpired', $ldapMetadata[] = new ReadonlyField(
'IsExpired',
Expand All @@ -66,24 +95,31 @@ public function updateCMSFields(FieldList $fields)
);
$fields->addFieldsToTab('Root.LDAP', $ldapMetadata);

if ($this->owner->IsImportedFromLDAP) {
// Transform the automatically mapped fields into read-only.
$mappings = Config::inst()->get('Member', 'ldap_field_mappings');
foreach ($mappings as $ldap=>$ss) {
$field = $fields->dataFieldByName($ss);
$message = '';
if ($this->owner->GUID && $this->owner->config()->update_ldap_from_local) {
$message = _t(
'LDAPMemberExtension.CHANGEFIELDSUPDATELDAP',
'Changing fields here will update them in LDAP.'
);
} elseif ($this->owner->GUID && !$this->owner->config()->update_ldap_from_local) {
// Transform the automatically mapped fields into read-only. This doesn't
// apply if updating LDAP from local is enabled, as changing data locally can be written back.
foreach ($this->owner->config()->ldap_field_mappings as $name) {
$field = $fields->dataFieldByName($name);
if (!empty($field)) {
// This messes up the Member_Validator, preventing the record from saving :-(
// $field->setReadonly(true);
$field->setTitle($field->Title() . _t('LDAPMemberExtension.IMPORTEDFIELD', ' (imported)'));
// Set to readonly, but not disabled so that the data is still sent to the
// server and doesn't break Member_Validator
$field->setReadonly(true);
$field->setTitle($field->Title()._t('LDAPMemberExtension.IMPORTEDFIELD', ' (imported)'));
}
}

// Display alert message at the top.
$message = _t(
'LDAPMemberExtension.INFOIMPORTED',
'This user is automatically imported from LDAP. ' .
'This user is automatically imported from LDAP. '.
'Manual changes to imported fields will be removed upon sync.'
);
}
if ($message) {
$fields->addFieldToTab(
'Root.Main',
new LiteralField(
Expand All @@ -95,14 +131,83 @@ public function updateCMSFields(FieldList $fields)
}
}

public function validate(ValidationResult $validationResult)
{
// We allow empty Username for registration purposes, as we need to
// create Member records with empty Username temporarily. Forms should explicitly
// check for Username not being empty if they require it not to be.
if (empty($this->owner->Username) || !$this->owner->config()->create_users_in_ldap) {
return;
}

if (!preg_match('/^[a-z0-9\.]+$/', $this->owner->Username)) {
$validationResult->error(
'Username must only contain lowercase alphanumeric characters and dots.',
'bad'
);
throw new ValidationException($validationResult);
}
}

/**
* Create the user in LDAP, provided this configuration is enabled
* and a username was passed to a new Member record.
*/
public function onBeforeWrite()
{
$service = Injector::inst()->get('LDAPService');
if (
!$service->enabled() ||
!$this->owner->config()->create_users_in_ldap ||
!$this->owner->Username ||
$this->owner->GUID
) {
return;
}

$service->createLDAPUser($this->owner);
}

/**
* Update the local data with LDAP, and ensure local membership is also set in
* LDAP too. This writes into LDAP, provided that feature is enabled.
*/
public function onAfterWrite()
{
$service = Injector::inst()->get('LDAPService');
if (
!$service->enabled() ||
!$this->owner->config()->update_ldap_from_local ||
!$this->owner->GUID
) {
return;
}

$service->updateLDAPFromMember($this->owner);
$service->updateLDAPGroupsForMember($this->owner);
}

public function onAfterDelete() {
$service = Injector::inst()->get('LDAPService');
if (
!$service->enabled() ||
!$this->owner->config()->delete_users_in_ldap ||
!$this->owner->GUID
) {
return;
}

$service->deleteLDAPMember($this->owner);
}

/**
* Triggered by {@link Member::logIn()} when successfully logged in,
* this will update the Member record from AD data.
*/
public function memberLoggedIn()
{
if ($this->owner->GUID) {
$this->ldapService->updateMemberFromLDAP($this->owner);
Injector::inst()->get('LDAPService')->updateMemberFromLDAP($this->owner);
}
}
}

0 comments on commit e4b5656

Please sign in to comment.