Skip to content

Commit

Permalink
feat: add fallback idp/mdl attribute mapping
Browse files Browse the repository at this point in the history
Used when the primary mapping does not match against any particular
user. This can be used in the case where attributes used for id
management are transitioned from one field to another, and allows for a
gradual non-disruptive rollover.
  • Loading branch information
keevan committed Nov 4, 2024
1 parent 2dc645c commit 2024110
Show file tree
Hide file tree
Showing 4 changed files with 102 additions and 22 deletions.
69 changes: 48 additions & 21 deletions classes/auth.php
Original file line number Diff line number Diff line change
Expand Up @@ -671,8 +671,9 @@ public function saml_login_complete($attributes) {
}

$attr = $this->config->idpattr;
$attrsecondary = $this->config->idpattrsecondary;
if (empty($attributes[$attr])) {
// Missing mapping IdP attribute. Login failed.
// Missing mapping IdP attribute (both primary and secondary). Login failed.
$event = \core\event\user_login_failed::create(['other' => ['username' => 'unknown',
'reason' => AUTH_LOGIN_NOUSER]]);
$event->trigger();
Expand All @@ -689,26 +690,15 @@ public function saml_login_complete($attributes) {

// Find Moodle user.
$user = false;
foreach ($attributes[$attr] as $uid) {
$insensitive = false;
$accentsensitive = true;
if ($this->config->tolower == saml2_settings::OPTION_TOLOWER_LOWER_CASE) {
$this->log(__FUNCTION__ . " to lowercase for $uid");
$uid = strtolower($uid);
}
if ($this->config->tolower == saml2_settings::OPTION_TOLOWER_CASE_INSENSITIVE) {
$this->log(__FUNCTION__ . " case insensitive compare for $uid");
$insensitive = true;
}
if ($this->config->tolower == saml2_settings::OPTION_TOLOWER_CASE_AND_ACCENT_INSENSITIVE) {
$this->log(__FUNCTION__ . " case and accent insensitive compare for $uid");
$insensitive = true;
$accentsensitive = false;
}
if ($user = user_extractor::get_user($this->config->mdlattr, $uid, $insensitive, $accentsensitive)) {
// We found a user.
break;
}

// Primary IdP attribute mapping.
if (!empty($attributes[$attr])) {
[$user, $uid] = $this->find_user_by_attributes($attributes[$attr], $this->config->mdlattr);
}

// Secondary IdP attribute mapping (if user not found yet).
if ($user === false && !empty($attributes[$attrsecondary])) {
[$user, $uid] = $this->find_user_by_attributes($attributes[$attrsecondary], $this->config->mdlattrsecondary);
}

// Moodle Workplace - Check IdP's tenant availability, for new user pre-allocate to tenant.
Expand Down Expand Up @@ -1343,4 +1333,41 @@ private function execute_callback($function, $file = 'lib.php') {
}
}
}

/**
* Find and return a user matched using a list of provided attributes, against a Moodle field.
*
* Applies any case matching settings configured.
*
* @param array $idpattrs
* @param string $mdlattr
* @return array false if no user found, otherwise the user object, and the $uid of the iterated user
*/
private function find_user_by_attributes(array $idpattrs, string $mdlattr): array {
$user = false;
$uid = null;
foreach ($idpattrs as $uid) {
$insensitive = false;
$accentsensitive = true;
if ($this->config->tolower == saml2_settings::OPTION_TOLOWER_LOWER_CASE) {
$this->log(__FUNCTION__ . " to lowercase for $uid");
$uid = strtolower($uid);
}
if ($this->config->tolower == saml2_settings::OPTION_TOLOWER_CASE_INSENSITIVE) {
$this->log(__FUNCTION__ . " case insensitive compare for $uid");
$insensitive = true;
}
if ($this->config->tolower == saml2_settings::OPTION_TOLOWER_CASE_AND_ACCENT_INSENSITIVE) {
$this->log(__FUNCTION__ . " case and accent insensitive compare for $uid");
$insensitive = true;
$accentsensitive = false;
}
if ($user = user_extractor::get_user($mdlattr, $uid, $insensitive, $accentsensitive)) {
// We found a user.
break;
}
}
return [$user, $uid];
}

}
4 changes: 4 additions & 0 deletions lang/en/auth_saml2.php
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@
$string['flagresponsetype_help'] = 'If access is blocked based on configured group restrictions, how should Moodle respond?';
$string['idpattr_help'] = 'Which IdP attribute should be matched against a Moodle user field?';
$string['idpattr'] = 'Mapping IdP';
$string['idpattrsecondary_help'] = 'When the primary IdP attribute does not match a user, map this field to the secondary Moodle mapped field.';
$string['idpattrsecondary'] = 'Secondary Mapping IdP';
$string['idpmetadata_badurl'] = 'Invalid metadata at {$a}';
$string['idpmetadata_help'] = 'To use multiple IdPs enter each public metadata url on a new line.<br/>To override a name, place text before the http. eg. "Forced IdP Name http://ssp.local/simplesaml/saml2/idp/metadata.php"';
$string['idpmetadata'] = 'IdP metadata xml OR public xml URL';
Expand All @@ -115,6 +117,8 @@
$string['manageidpsheading'] = 'Manage available Identity Providers (IdPs)';
$string['mdlattr_help'] = 'Which Moodle user field should the IdP attribute be matched to?';
$string['mdlattr'] = 'Mapping Moodle';
$string['mdlattrsecondary_help'] = 'Which Moodle user field should the IdP secondary attribute be matched to?';
$string['mdlattrsecondary'] = 'Secondary Mapping Moodle';
$string['wantassertionssigned'] = 'Want assertions signed';
$string['wantassertionssigned_help'] = 'Whether assertions received by this SP must be signed';
$string['assertionsconsumerservices'] = 'Assertions consumer services';
Expand Down
14 changes: 14 additions & 0 deletions settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,20 @@
saml2_settings::OPTION_TOLOWER_EXACT,
$toloweroptions));

// IDP attribute (secondary).
$settings->add(new admin_setting_configtext(
'auth_saml2/idpattrsecondary',
get_string('idpattrsecondary', 'auth_saml2'),
get_string('idpattrsecondary_help', 'auth_saml2'),
'', PARAM_TEXT));

// Moodle Field (secondary).
$settings->add(new admin_setting_configselect(
'auth_saml2/mdlattrsecondary',
get_string('mdlattrsecondary', 'auth_saml2'),
get_string('mdlattrsecondary_help', 'auth_saml2'),
'', user_fields::get_supported_fields()));

// Requested Attributes.
$settings->add(new admin_setting_configtextarea(
'auth_saml2/requestedattributes',
Expand Down
37 changes: 36 additions & 1 deletion tests/auth_test.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
* @copyright 2021 Moodle Pty Ltd <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class auth_saml2_test extends \advanced_testcase {
class auth_test extends \advanced_testcase {
/**
* Set up
*/
Expand Down Expand Up @@ -435,6 +435,41 @@ public function test_saml_login_complete_missing_idpattr(): void {
$this->assertEquals(AUTH_LOGIN_NOUSER, $event->get_data()['other']['reason']);
}

public function test_saml_login_complete_secondary_mapping_used(): void {
global $USER;

$attribs = [
'uid' => ['doesnotmatch'],
'email' => ['[email protected]'],
'someidfield' => ['must-match-12345'],
];

$user = $this->getDataGenerator()->create_user([
'auth' => 'saml2',
'email' => '[email protected]',
'idnumber' => 'must-match-12345',
]);

// The primary was set up to fail.
set_config('idpattr', 'uid', 'auth_saml2');
set_config('mdlattr', 'email', 'auth_saml2');
// The secondary mapping should match and map to the generated user.
set_config('idpattrsecondary', 'someidfield', 'auth_saml2');
set_config('mdlattrsecondary', 'idnumber', 'auth_saml2');

// Sanity check.
$this->assertFalse(isloggedin());
$this->assertNotEquals($attribs['email'][0], $user->email);

// Try to login, suppress output.
$auth = new \auth_saml2\auth();
@$auth->saml_login_complete($attribs);

// Check global object, make sure the created user is the one logged in, despite other non-matching attributes provided.
$this->assertEquals($user->id, $USER->id);
$this->assertEquals($user->username, $USER->username);
}

public function test_saml_login_complete_group_restriction(): void {
$attribs = [
'uid' => ['samlu1'],
Expand Down

0 comments on commit 2024110

Please sign in to comment.