Skip to content

Commit 306a852

Browse files
committed
Implement proper group mapping via SAML
Fixes #561 As stated in the issue, it's not desirable to have a group called `admin` in the SAML backend which doesn't indicate to which service admin permissions are granted. This is orthogonal to `saml-attribute-mapping-group_mapping` which simply maps all groups from a SAML attribute to Nextcloud groups, i.e. the attribute's value MUST contain a group called `admin` to make sure that users get admin rights in Nextcloud. When enabled, the name of (another) attribute must be specified which contains a list of SAML-specific groups, e.g. ["nextcloud-admins", "nextcloud-marketing"] that can be mapped to e.g. ["admin", "marketing"]
1 parent 7a78b3c commit 306a852

File tree

4 files changed

+175
-13
lines changed

4 files changed

+175
-13
lines changed

js/admin.js

+100
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,99 @@ $(function() {
223223
OCA.User_SAML.Admin.resetSettings();
224224
});
225225

226+
function isGroupMappingValid() {
227+
if (!$('#user-saml-group-mapping-field-name').val()) {
228+
return false;
229+
}
230+
var allMappingsDeclared = $('#saml-mapping-items')
231+
.children('p')
232+
.children('input')
233+
.map(function () { return $(this).val(); })
234+
.get()
235+
.every(function (x) { return x; });
236+
if (!allMappingsDeclared) {
237+
return false;
238+
}
239+
return true;
240+
}
241+
242+
function updateGroupMappings() {
243+
OCA.User_SAML.Admin.setSamlConfigValue(
244+
'group-mapping',
245+
'field-mappings',
246+
JSON.stringify(
247+
$('#saml-mapping-items').children('p').map(function () {
248+
return [$(this).children('.required').map(function () { return $(this).val() }).get()];
249+
}).get()
250+
)
251+
);
252+
}
253+
254+
$(document).on('change', '#saml-mapping-items input', function () {
255+
if (isGroupMappingValid()) {
256+
$('#user-saml-settings-group-mapping-incomplete').addClass('hidden');
257+
OCA.User_SAML.Admin.setSamlConfigValue('group-mapping', 'enable', '1');
258+
updateGroupMappings();
259+
} else {
260+
$('#user-saml-settings-group-mapping-incomplete').removeClass('hidden');
261+
}
262+
});
263+
264+
$('#saml-add-mapping').click(function(e) {
265+
e.preventDefault();
266+
$('#saml-mapping-items').append("\
267+
<p class=\"saml-group-mapping\">\
268+
<input type=\"text\" placeholder=\"SAML group\" class=\"required\" />\
269+
<input type=\"text\" placeholder=\"Nextcloud pendant\" class=\"required\" />\
270+
<button class=\"user-saml-remove-mapping\">Remove mapping</button>\
271+
</p>");
272+
$('#saml-group-initial').clone().appendTo('#saml-mapping-items');
273+
$('#user-saml-settings-group-mapping-incomplete').removeClass('hidden');
274+
});
275+
276+
$(document).on('click', '.user-saml-remove-mapping', function(e) {
277+
e.preventDefault();
278+
$(this).closest('.saml-group-mapping').remove();
279+
if (isGroupMappingValid()) {
280+
$('#user-saml-settings-group-mapping-incomplete').addClass('hidden');
281+
OCA.User_SAML.Admin.setSamlConfigValue('group-mapping', 'enable', '1');
282+
updateGroupMappings();
283+
}
284+
});
285+
286+
$('#user-saml-group-mapping-check').change(function() {
287+
$(this).val($(this).val() === '0' ? '1' : '0');
288+
if ($(this).val() == '1') {
289+
if (!isGroupMappingValid()) {
290+
$('#user-saml-settings-group-mapping-incomplete').removeClass('hidden');
291+
} else {
292+
OCA.User_SAML.Admin.setSamlConfigValue('group-mapping', 'enable', '1');
293+
}
294+
$('#user-saml-group-mapping-field-name').removeAttr('disabled');
295+
$('#saml-add-mapping').removeAttr('disabled');
296+
$('#saml-mapping-items').children('p').children('input, button').removeAttr('disabled');
297+
} else {
298+
OCA.User_SAML.Admin.setSamlConfigValue('group-mapping', 'enable', '0');
299+
$('#user-saml-group-mapping-field-name').attr('disabled', 'disabled');
300+
$('#saml-add-mapping').attr('disabled', 'disabled');
301+
$('#user-saml-settings-group-mapping-incomplete').addClass('hidden');
302+
$('#saml-mapping-items').children('p').children('input, button').attr('disabled', 'disabled');
303+
}
304+
});
305+
306+
$('#user-saml-group-mapping-field-name').change(function() {
307+
var value = $(this).val();
308+
if (value) {
309+
if (isGroupMappingValid()) {
310+
$('#user-saml-settings-group-mapping-incomplete').addClass('hidden');
311+
}
312+
OCA.User_SAML.Admin.setSamlConfigValue('group-mapping', 'enable', '1');
313+
OCA.User_SAML.Admin.setSamlConfigValue('group-mapping', 'saml-field', value);
314+
} else {
315+
$('#user-saml-settings-group-mapping-incomplete').removeClass('hidden');
316+
}
317+
});
318+
226319
var switchProvider = function(providerId) {
227320
$('.account-list li').removeClass('active');
228321
$('.account-list li[data-id="' + providerId + '"]').addClass('active');
@@ -446,6 +539,13 @@ $(function() {
446539
text = 'Show attribute mapping settings ...';
447540
}
448541
break;
542+
case 'user-saml-group-mapping':
543+
if (nextSibling.hasClass('hidden')) {
544+
text = 'Hide group mapping settings ...';
545+
} else {
546+
text = 'Show group mapping settings ...';
547+
}
548+
break;
449549
}
450550
el.html(t('user_saml', text));
451551

lib/SAMLSettings.php

+3
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@ class SAMLSettings {
6868
'sp-x509cert',
6969
'sp-name-id-format',
7070
'sp-privateKey',
71+
'group-mapping-enable',
72+
'group-mapping-saml-field',
73+
'group-mapping-field-mappings',
7174
];
7275

7376
/** @var IURLGenerator */

lib/UserBackend.php

+41-13
Original file line numberDiff line numberDiff line change
@@ -659,24 +659,52 @@ public function updateAttributes($uid,
659659
$user->setQuota($newQuota);
660660
}
661661

662+
if ($this->getGroupMappingSetting('enable') === '1') {
663+
$fieldName = $this->getGroupMappingSetting('saml-field');
664+
$groupMappings = json_decode($this->getGroupMappingSetting('field-mappings'), true);
665+
$newMappedGroups = [];
666+
$samlGroups = $attributes[$fieldName] ?? [];
667+
foreach ($samlGroups as $samlGroup) {
668+
foreach ($groupMappings as $mapping) {
669+
list($saml, $nextcloud) = $mapping;
670+
if ($saml === $samlGroup) {
671+
$newMappedGroups[] = $nextcloud;
672+
}
673+
}
674+
}
675+
if ($newMappedGroups !== []) {
676+
$this->updateGroups($newMappedGroups, $user);
677+
}
678+
}
679+
662680
if ($newGroups !== null) {
663-
$groupManager = $this->groupManager;
664-
$oldGroups = $groupManager->getUserGroupIds($user);
681+
$this->updateGroups($newGroups, $user);
682+
}
683+
}
684+
}
665685

666-
$groupsToAdd = array_unique(array_diff($newGroups, $oldGroups));
667-
$groupsToRemove = array_diff($oldGroups, $newGroups);
686+
private function getGroupMappingSetting($key) {
687+
$id = $this->settings->getProviderId();
688+
$settings = $this->settings->get($id);
689+
return $settings['group-mapping-' . $key] ?? null;
690+
}
668691

669-
foreach ($groupsToAdd as $group) {
670-
if (!($groupManager->groupExists($group))) {
671-
$groupManager->createGroup($group);
672-
}
673-
$groupManager->get($group)->addUser($user);
674-
}
692+
private function updateGroups(array $newGroups, $user) {
693+
$groupManager = $this->groupManager;
694+
$oldGroups = $groupManager->getUserGroupIds($user);
675695

676-
foreach ($groupsToRemove as $group) {
677-
$groupManager->get($group)->removeUser($user);
678-
}
696+
$groupsToAdd = array_unique(array_diff($newGroups, $oldGroups));
697+
$groupsToRemove = array_diff($oldGroups, $newGroups);
698+
699+
foreach ($groupsToAdd as $group) {
700+
if (!($groupManager->groupExists($group))) {
701+
$groupManager->createGroup($group);
679702
}
703+
$groupManager->get($group)->addUser($user);
704+
}
705+
706+
foreach ($groupsToRemove as $group) {
707+
$groupManager->get($group)->removeUser($user);
680708
}
681709
}
682710

templates/admin.php

+31
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,36 @@
198198
</div>
199199
</div>
200200

201+
<div id="user-saml-group-mapping">
202+
<h3><?php p($l->t('Group mapping')) ?></h3>
203+
<p>
204+
<?php p($l->t('Map groups from a specified SAML attribute to existing Nextcloud groups')) ?>
205+
<span class="toggle"><?php p($l->t('Show group mapping settings ...')) ?></span>
206+
</p>
207+
<div class="ident hidden">
208+
<p>
209+
<input type="checkbox" id="user-saml-group-mapping-check" name="user-saml-group-mapping-check" value="<?php p($_['config']['group-mapping-enable'] ?? '0') ?>" />
210+
<label for="user-saml-group-mapping-check"><?php p($l->t('Whether to enable group mapping')) ?></label>
211+
</p>
212+
<p>
213+
<input <?php if ('0' === $_['config']['group-mapping-enable'] ?? '0'): ?>disabled="disabled"<?php endif; ?> id="user-saml-group-mapping-field-name" type="text" placeholder="<?php p($l->t('SAML group attribute name')) ?>" class="required" value="<?php p($_['config']['group-mapping-saml-field'] ?? '') ?>" />
214+
</p>
215+
<div>
216+
<h4>Mappings</h4>
217+
<div id="saml-mapping-items">
218+
<?php foreach (json_decode($_['config']['group-mapping-field-mappings'] ?? '') as $mapping): ?>
219+
<p class="saml-group-mapping">
220+
<input type="text" placeholder="SAML group" class="required" value="<?php p($mapping[0]) ?>" />
221+
<input type="text" placeholder="Nextcloud pendant" class="required" value="<?php p($mapping[1]) ?>" />
222+
<button class="user-saml-remove-mapping">Remove mapping</button>
223+
</p>
224+
<?php endforeach; ?>
225+
</div>
226+
<button id="saml-add-mapping" <?php if ('0' === $_['config']['group-mapping-enable'] ?? '0'): ?>disabled="disabled"<?php endif; ?>>Add Mapping</button>
227+
</div>
228+
</div>
229+
</div>
230+
201231
<a id="get-metadata" data-base="<?php p(\OC::$server->getURLGenerator()->linkToRoute('user_saml.SAML.getMetadata')); ?>"
202232
href="<?php p(\OC::$server->getURLGenerator()->linkToRoute('user_saml.SAML.getMetadata', ['idp' => $_['providers'][0]['id']])) ?>" class="button">
203233
<?php p($l->t('Download metadata XML')) ?>
@@ -207,6 +237,7 @@
207237

208238

209239
<span class="warning hidden" id="user-saml-settings-incomplete"><?php p($l->t('Metadata invalid')) ?></span>
240+
<span class="warning hidden" id="user-saml-settings-group-mapping-incomplete"><?php p($l->t('Group mapping invalid')) ?></span>
210241
<span class="success hidden" id="user-saml-settings-complete"><?php p($l->t('Metadata valid')) ?></span>
211242
</div>
212243
</form>

0 commit comments

Comments
 (0)