From 9bc4194e555af70d3e522519684bdb1855ce064d Mon Sep 17 00:00:00 2001
From: Martin van Es <martin@mrvanes.com>
Date: Tue, 19 Jan 2016 15:59:21 +0100
Subject: [PATCH] Add GenerateAffiliation Authentication Processing Filter and
 unittest. GenerateAffiliation generates an attribute with fixed values based
 on (multiple) values found in a source attribute. In short: if member of
 group "A" create target attribute with value "student".

---
 .../docs/authproc_generateaffiliation.txt     |  61 ++++++++
 .../lib/Auth/Process/GenerateAffiliation.php  | 110 ++++++++++++++
 .../Auth/Process/GenerateAffiliationTest.php  | 140 ++++++++++++++++++
 3 files changed, 311 insertions(+)
 create mode 100644 modules/core/docs/authproc_generateaffiliation.txt
 create mode 100644 modules/core/lib/Auth/Process/GenerateAffiliation.php
 create mode 100644 tests/modules/core/lib/Auth/Process/GenerateAffiliationTest.php

diff --git a/modules/core/docs/authproc_generateaffiliation.txt b/modules/core/docs/authproc_generateaffiliation.txt
new file mode 100644
index 000000000..37d7e427d
--- /dev/null
+++ b/modules/core/docs/authproc_generateaffiliation.txt
@@ -0,0 +1,61 @@
+`core:GenerateAffiliation`
+===================
+
+Filter that generate an attribute to the user based on value(s) in another attribute.
+
+Default member attribute is memberOf, default target attribute is eduPersonAffiliation.
+%replace can be used to replace member attribute with target attribute, otherwise both will exist
+after processing filter. If the member attribute does not exist, nothing will be done or replaced.
+
+
+Examples
+--------
+
+Add student affiliation based on LDAP groupmembership
+Will add eduPersonAffiliation containing value "student" if memberOf attribute contains 'cn=student,o=some,o=organization,dc=org'.
+
+    'authproc' => array(
+        50 => array(
+            'class' => 'core:GenerateAffiliation',
+            'values' => array(
+                'student' => array(
+                    'cn=student,o=some,o=organization,dc=org',
+                ),
+        ),
+    ),
+
+
+Add student and employee affiliation based on LDAP groupmembership
+
+    'authproc' => array(
+        50 => array(
+            'class' => 'core:GenerateAffiliation',
+            'values' => array(
+                'student' => array(
+                    'cn=student,o=some,o=organization,dc=org',
+                ),
+                'employee' => array(
+                    'cn=employees,o=some,o=organization,dc=org',
+                ),
+        ),
+    ),
+
+Different memberof and target attributes, replace member attribute
+Will add 'affiliation' containing 'student' and/or 'employee' depending on the values in 'groups' attribute and remove the latter.
+
+    'authproc' => array(
+        50 => array(
+            'class' => 'core:GenerateAffiliation',
+            '%replace',
+            'attributename' => 'affiliation',
+            'memberattribute' => 'groups',
+            'values' => array(
+                'student' => array(
+                    'cn=student,o=some,o=organization,dc=org',
+                ),
+                'employee' => array(
+                    'cn=employees,o=some,o=organization,dc=org',
+                ),
+        ),
+    ),
+    
diff --git a/modules/core/lib/Auth/Process/GenerateAffiliation.php b/modules/core/lib/Auth/Process/GenerateAffiliation.php
new file mode 100644
index 000000000..ad563087f
--- /dev/null
+++ b/modules/core/lib/Auth/Process/GenerateAffiliation.php
@@ -0,0 +1,110 @@
+<?php
+
+/**
+ * Filter to generate affiliation(s) based on groupmembership attribute
+ *
+ * @author Martin van Es, m7
+ * @package simpleSAMLphp
+ */
+class sspmod_core_Auth_Process_GenerateAffiliation extends SimpleSAML_Auth_ProcessingFilter {
+
+    /**
+    * The attributename we should assign affiliations to (target)
+    */
+    private $attributename = 'eduPersonAffiliation';
+
+    /**
+    * The attributename we should generate affiliations from
+    */
+    private $memberattribute = 'memberOf';
+
+    /**
+    * The required $memberattribute values and target affiliations
+    */
+    private $values = array();
+    
+    /**
+    * Wether $memberattribute should be replaced by target attribute
+    */
+    private $replace = FALSE;
+    
+    /**
+    * Initialize this filter.
+    *
+    * @param array $config  Configuration information about this filter.
+    * @param mixed $reserved  For future use.
+    */
+    public function __construct($config, $reserved) {
+        parent::__construct($config, $reserved);
+
+        assert('is_array($config)');
+
+        /* Validate configuration. */
+        foreach ($config as $name => $value) {
+            if (is_int($name)) {
+                // check if this is an option
+                if ($value === '%replace') {
+                        $this->replace = TRUE;
+                } else {
+                        throw new SimpleSAML_Error_Exception('Unknown flag : ' . var_export($value, TRUE));
+                }
+                continue;
+            }
+
+            // Set attributename
+            if ($name === 'attributename') {
+                $this->attributename = $value;
+            }
+
+            // Set memberattribute
+            if ($name === 'memberattribute') {
+                $this->memberattribute = $value;
+            }
+        
+            // Set values
+            if ($name === 'values') {
+                $this->values = $value;
+            }
+        }
+    }
+
+
+    /**
+        * Apply filter to add groups attribute.
+        *
+        * @param array &$request  The current request
+        */
+    public function process(&$request) {
+        assert('is_array($request)');
+        assert('array_key_exists("Attributes", $request)');
+        $attributes =& $request['Attributes'];
+
+        $affiliations = array();
+
+        if (array_key_exists($this->memberattribute, $attributes)) {
+            $memberof = $attributes[$this->memberattribute];
+
+            if (is_array($memberof)) {
+                foreach ($this->values as $value => $require) {
+                    if (count(array_intersect($require, $memberof)) > 0) {
+                        SimpleSAML_Logger::debug('GenerateAffiliation - intersect match for ' . $value);
+                        $affiliations[] = $value;
+                    }
+                }
+            }
+
+            if (count($affiliations) > 0) {
+                    $attributes[$this->attributename] = $affiliations;
+            }
+
+            if ($this->replace) {
+                unset($attributes[$this->memberattribute]);
+            }
+
+        } else {
+            SimpleSAML_Logger::warning('GenerateAffiliation - memberattribute does not exist: ' . $this->memberattribute);            
+        }
+    }
+}
+
+?>
\ No newline at end of file
diff --git a/tests/modules/core/lib/Auth/Process/GenerateAffiliationTest.php b/tests/modules/core/lib/Auth/Process/GenerateAffiliationTest.php
new file mode 100644
index 000000000..d1b17e8a4
--- /dev/null
+++ b/tests/modules/core/lib/Auth/Process/GenerateAffiliationTest.php
@@ -0,0 +1,140 @@
+<?php
+
+/**
+ * Test for the core:GenerateAffiliation filter.
+ */
+class Test_Core_Auth_Process_GenerateAffiliation extends PHPUnit_Framework_TestCase
+{
+
+    /**
+     * Helper function to run the filter with a given configuration.
+     *
+     * @param array $config  The filter configuration.
+     * @param array $request  The request state.
+     * @return array  The state array after processing.
+     */
+    private static function processFilter(array $config, array $request) {
+        $filter = new sspmod_core_Auth_Process_GenerateAffiliation($config, NULL);
+        $filter->process($request);
+        return $request;
+    }
+
+    /**
+     * Test the most basic functionality.
+     */
+    public function testBasic() {
+        $config = array(
+            'values' => array(
+                'target' => array(
+                    'source',
+                ),
+            ),
+        );
+        $request = array(
+            'Attributes' => array(
+                'memberOf' => array('source'),
+            ),
+        );
+        $result = self::processFilter($config, $request);
+        $attributes = $result['Attributes'];
+        $this->assertArrayHasKey('eduPersonAffiliation', $attributes);
+        $this->assertArrayHasKey('memberOf', $attributes);
+        $this->assertEquals($attributes['eduPersonAffiliation'], array('target'));
+    }
+
+    /**
+     * Test the %replace functionality.
+     */
+    public function testReplace() {
+        $config = array(
+            '%replace',
+            'values' => array(
+                'target' => array(
+                    'source',
+                ),
+            ),
+        );
+        $request = array(
+            'Attributes' => array(
+                'memberOf' => array('source'),
+            ),
+        );
+        $result = self::processFilter($config, $request);
+        $attributes = $result['Attributes'];
+        $this->assertArrayHasKey('eduPersonAffiliation', $attributes);
+        $this->assertArrayNotHasKey('memberOf', $attributes);
+        $this->assertEquals($attributes['eduPersonAffiliation'], array('target'));
+    }
+
+    /**
+     * Test the different Attribute configurations.
+     */
+    public function testAttributeConfig() {
+        $config = array(
+            'attributename' => 'affiliation',
+            'memberattribute' => 'group',
+            'values' => array(
+                'target' => array(
+                    'source',
+                ),
+            ),
+        );
+        $request = array(
+            'Attributes' => array(
+                'group' => array('source'),
+            ),
+        );
+        $result = self::processFilter($config, $request);
+        $attributes = $result['Attributes'];
+        $this->assertArrayHasKey('affiliation', $attributes);
+        $this->assertEquals($attributes['affiliation'], array('target'));
+    }
+
+    
+    /**
+     * Test unknown flag Exception
+     *
+     * @expectedException Exception
+     */
+    public function testUnknownFlag() {
+        $config = array(
+            '%test',
+            'values' => array(
+                'target' => array(
+                    'source',
+                ),
+            ),
+        );
+        $request = array(
+            'Attributes' => array(
+                'memberOf' => array('source'),
+            ),
+        );
+        $result = self::processFilter($config, $request);
+    }
+
+    /**
+     * Test missing member attribute
+     *
+     */
+    public function testMissingMemberAttribute() {
+        $config = array(
+            '%replace',
+            'values' => array(
+                'target' => array(
+                    'source',
+                ),
+            ),
+        );
+        $request = array(
+            'Attributes' => array(
+                'test' => array('source'),
+            ),
+        );
+        $result = self::processFilter($config, $request);
+        $attributes = $result['Attributes'];
+        $this->assertArrayHasKey('test', $attributes);
+        $this->assertArrayNotHasKey('eduPersonAffiliation', $attributes);
+        $this->assertEquals($attributes['test'], array('source'));
+    }
+}
\ No newline at end of file
-- 
GitLab