Skip to content

Commit a380ee3

Browse files
committed
feat: group auto assignment and leaving based on role-to-group association
1 parent 852031b commit a380ee3

5 files changed

Lines changed: 140 additions & 8 deletions

File tree

lib/controllers.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
const fetch = require('node-fetch');
44

55
const db = require.main.require('./src/database');
6+
const groups = require.main.require('./src/groups');
67
const slugify = require.main.require('./src/slugify');
78
const helpers = require.main.require('./src/controllers/helpers');
89

@@ -11,10 +12,23 @@ const main = require('../library');
1112
const Controllers = module.exports;
1213

1314
Controllers.renderAdminPage = async (req, res) => {
15+
const main = require('../library');
1416
const strategies = await main.listStrategies();
17+
let groupNames = await db.getSortedSetRange('groups:createtime', 0, -1);
18+
groupNames = groupNames.filter(name => (
19+
name !== 'registered-users' &&
20+
name !== 'verified-users' &&
21+
name !== 'unverified-users' &&
22+
name !== groups.BANNED_USERS &&
23+
!groups.isPrivilegeGroup(name)
24+
));
25+
const associations = await main.getAssociations();
26+
1527
res.render('admin/plugins/sso-oauth2-multiple', {
1628
title: 'Multiple OAuth2',
1729
strategies,
30+
associations,
31+
groupNames,
1832
});
1933
};
2034

library.js

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ const winston = module.parent.require('winston');
77
const db = require.main.require('./src/database');
88
const user = require.main.require('./src/user');
99
const plugins = require.main.require('./src/plugins');
10+
const meta = require.main.require('./src/meta');
11+
const groups = require.main.require('./src/groups');
1012
const authenticationController = require.main.require('./src/controllers/authentication');
1113
const routeHelpers = require.main.require('./src/routes/helpers');
1214

@@ -101,6 +103,7 @@ OAuth.loadStrategies = async (strategies) => {
101103

102104
winston.verbose(`[plugin/sso-oauth2-multiple] Successful login to uid ${user.uid} via ${name} (remote id ${id})`);
103105
authenticationController.onSuccessfulLogin(req, user.uid);
106+
OAuth.assignGroups({ provider: name, user, profile });
104107
done(null, user);
105108

106109
plugins.hooks.fire('action:oauth2.login', { name, user, profile });
@@ -155,14 +158,15 @@ OAuth.getUserProfile = function (name, userRoute, accessToken, done) {
155158
OAuth.parseUserReturn = async (provider, profile) => {
156159
const {
157160
id, sub, name, nickname, preferred_username, picture,
158-
email, /* , email_verified */
161+
roles, email, /* , email_verified */
159162
} = profile;
160163
const { usernameViaEmail, idKey } = await OAuth.getStrategy(provider);
161164
const normalized = {
162165
provider,
163166
id: profile[idKey] || id || sub,
164167
displayName: nickname || preferred_username || name,
165168
picture,
169+
roles,
166170
email,
167171
};
168172

@@ -173,6 +177,19 @@ OAuth.parseUserReturn = async (provider, profile) => {
173177
return normalized;
174178
};
175179

180+
OAuth.getAssociations = async () => {
181+
let { roles, groups } = await meta.settings.get('sso-oauth2-multiple');
182+
if (!roles || !groups) {
183+
return [];
184+
}
185+
186+
groups = groups.split(',');
187+
return roles.split(',').map((role, idx) => ({
188+
role,
189+
group: groups[idx],
190+
}));
191+
};
192+
176193
OAuth.login = async (payload) => {
177194
let uid = await OAuth.getUidByOAuthid(payload.name, payload.oAuthid);
178195
if (uid !== null) {
@@ -202,6 +219,30 @@ OAuth.login = async (payload) => {
202219
return { uid };
203220
};
204221

222+
OAuth.assignGroups = async ({ user, profile }) => {
223+
if (!profile.roles || !Array.isArray(profile.roles)) {
224+
return;
225+
}
226+
227+
const { uid } = user;
228+
const associations = await OAuth.getAssociations();
229+
const { toJoin, toLeave } = associations.reduce((memo, { role, group }) => {
230+
if (profile.roles.includes(role)) {
231+
memo.toJoin.push(group);
232+
} else {
233+
memo.toLeave.push(group);
234+
}
235+
236+
return memo;
237+
}, { toJoin: [], toLeave: [] });
238+
if (toLeave.length) {
239+
winston.verbose(`[plugins/sso-auth0] uid ${uid} now leaving ${toLeave.length} these user groups: ${toLeave.join(', ')}`);
240+
}
241+
await groups.leave(toLeave, uid);
242+
await groups.join(toJoin, uid);
243+
winston.verbose(`[plugins/sso-auth0] uid ${uid} now a part of ${toJoin.length} these user groups: ${toJoin.join(', ')}`);
244+
};
245+
205246
OAuth.getUidByOAuthid = async (name, oAuthid) => db.getObjectField(`${name}Id:uid`, oAuthid);
206247

207248
OAuth.deleteUserData = async (data) => {

static/lib/admin.js

Lines changed: 56 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,10 @@ import { alert as bootboxAlert, confirm } from 'bootbox';
77
import { get, post, del } from 'api';
88
import { alert, error } from 'alerts';
99
import { render } from 'benchpress';
10+
import { save, load } from 'settings';
1011

1112
// eslint-disable-next-line import/prefer-default-export
1213
export function init() {
13-
// settings.load('sso-oauth2-multiple', $('.sso-oauth2-multiple-settings'));
14-
// $('#save').on('click', saveSettings);
15-
const saveEl = document.getElementById('save');
16-
if (saveEl) {
17-
saveEl.classList.toggle('d-none', true);
18-
}
19-
2014
const formEl = document.querySelector('.sso-oauth2-multiple-settings');
2115
formEl.addEventListener('click', async (e) => {
2216
const subselector = e.target.closest('[data-action]');
@@ -85,6 +79,61 @@ export function init() {
8579
}
8680
}
8781
});
82+
83+
handleSettingsForm();
84+
handleAssociations();
85+
}
86+
87+
function handleSettingsForm() {
88+
load('sso-oauth2-multiple', $('.sso-oauth2-multiple-settings'), () => {
89+
const fieldset = document.getElementById('associations');
90+
const selectEls = fieldset.querySelectorAll('select[data-value]');
91+
if (selectEls.length) {
92+
selectEls.forEach((el) => {
93+
const value = el.getAttribute('data-value');
94+
const optionEl = el.querySelector(`option[value="${value}"]`);
95+
optionEl.selected = true;
96+
});
97+
}
98+
99+
// settings.load shoves all the values into the first association input, so
100+
// the roles all start out disabled so that they don't get overridden
101+
const domainEls = fieldset.querySelectorAll('input[disabled]');
102+
domainEls.forEach((el) => {
103+
el.disabled = false;
104+
});
105+
});
106+
107+
$('#save').on('click', () => {
108+
save('sso-oauth2-multiple', $('.sso-oauth2-multiple-settings')); // pass in a function in the 3rd parameter to override the default success/failure handler
109+
});
110+
}
111+
112+
function handleAssociations() {
113+
const addEl = document.querySelector('[data-action="add"]');
114+
const fieldset = document.getElementById('associations');
115+
console.log(addEl, fieldset);
116+
if (!addEl || !fieldset) {
117+
return;
118+
}
119+
120+
addEl.addEventListener('click', async () => {
121+
let html = await render('partials/group-association-field', {
122+
groupNames: ajaxify.data.groupNames,
123+
});
124+
html = new DOMParser().parseFromString(html, 'text/html').body.childNodes;
125+
fieldset.append(...html);
126+
});
127+
128+
fieldset.addEventListener('click', (e) => {
129+
const subselector = e.target.closest('[data-action="remove"]');
130+
if (!subselector) {
131+
return;
132+
}
133+
134+
const row = subselector.closest('.association');
135+
row.remove();
136+
});
88137
}
89138

90139
function handleEditStrategy(ok) {

static/templates/admin/plugins/sso-oauth2-multiple.tpl

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,22 @@
5050
</tfoot>
5151
</table>
5252
</div>
53+
54+
<div class="mb-4">
55+
<h5>Role to Group Associations</h5>
56+
<p>
57+
If the OAuth2 provider sends back a <code>roles</code> property in the User Info endpoint,
58+
the user can be assigned to a specific user group based on a configured association below.
59+
</p>
60+
61+
<fieldset id="associations">
62+
{{{ each associations }}}
63+
<!-- IMPORT partials/group-association-field.tpl -->
64+
{{{ end }}}
65+
</fieldset>
66+
67+
<button type="button" class="btn btn-primary" data-action="add">Add association</button>
68+
</div>
5369
</form>
5470
</div>
5571

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<div class="mb-3 association">
2+
<div class="input-group">
3+
<input type="text" name="roles" class="form-control" placeholder="Role" value="{./role}" {{{ if ./role }}}disabled{{{ end }}}>
4+
<span class="input-group-text">&rarr;</span>
5+
<select class="form-control" name="groups" data-value="{./group}">
6+
{{{ each groupNames }}}
7+
<option value="{@value}">{@value}</option>
8+
{{{ end }}}
9+
</select>
10+
<button class="btn" type="button" data-action="remove"><i class="fa fa-trash text-danger"></i></button>
11+
</div>
12+
</div>

0 commit comments

Comments
 (0)