Skip to content

Commit 4ec8f31

Browse files
authored
Merge pull request #506 from bheesham/default-deny
Deny if apps are not explicitly listed in apps.yml
2 parents 1d7a441 + af74027 commit 4ec8f31

3 files changed

Lines changed: 133 additions & 85 deletions

File tree

tf/actions/accessRules.js

Lines changed: 113 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -114,13 +114,51 @@ exports.onExecutePostLogin = async (event, api) => {
114114
return A.some((element) => B.includes(element));
115115
};
116116

117-
// Process the access cache decision
118-
const access_decision = (access_rules, access_file_conf) => {
117+
// Return a single identity by connection name, from the user structure
118+
const getProfileData = (connection) => {
119+
var i = 0;
120+
for (i = 0; i < event.user.identities.length; i++) {
121+
var cid = event.user.identities[i];
122+
if (cid.connection === connection) {
123+
return cid.profileData;
124+
}
125+
}
126+
return undefined;
127+
};
128+
129+
// Sometimes we need to add custom claims to the various tokens we hand
130+
// out.
131+
const groupsSetCustomClaims = (groups) => {
132+
// If the only scopes requested are neither profile nor any scope beginning with
133+
// https:// then do not overload with custom claims
134+
const scopes_requested = event.transaction.requested_scopes || [];
135+
136+
let fixup_needed = (scope) => {
137+
return scope === "profile" || scope.startsWith("https://");
138+
};
139+
140+
if (scopes_requested.some(fixup_needed)) {
141+
console.log(
142+
`Client ${event.client.client_id} requested ${scopes_requested}, therefore adding custom claims`
143+
);
144+
api.idToken.setCustomClaim("email_aliases", undefined);
145+
api.idToken.setCustomClaim("dn", undefined);
146+
api.idToken.setCustomClaim("organizationUnits", undefined);
147+
api.idToken.setCustomClaim(`${namespace}/groups`, groups);
148+
149+
const claimMsg =
150+
"Please refer to https://github.com/mozilla-iam/person-api in order to query Mozilla IAM CIS user profile data";
151+
api.idToken.setCustomClaim(`${namespace}/README_FIRST`, claimMsg);
152+
}
153+
};
154+
155+
// Collect all variations of groups and merge them together for access
156+
// evaluation.
157+
const groupsGather = () => {
119158
// Ensure we have the correct group data
120159
const app_metadata_groups = event.user.app_metadata.groups || [];
121160
const ldap_groups = event.user.ldap_groups || [];
122161
const user_groups = event.user.groups || [];
123-
124162
// With account linking its possible that LDAP is not the main account on contributor LDAP accounts
125163
// Here we iterate over all possible user identities and build an array of all groups from them
126164
let _identity;
@@ -138,54 +176,53 @@ exports.onExecutePostLogin = async (event, api) => {
138176
}
139177
}
140178
}
141-
142-
// Collect all variations of groups and merge them together for access evaluation
143-
let groups = [
179+
const all_groups = [
144180
...user_groups,
145181
...app_metadata_groups,
146182
...ldap_groups,
147183
...identityGroups,
184+
// A default group, added to everyone.
185+
"everyone",
148186
];
149-
150-
// Inject the everyone group and filter for duplicates
151-
groups.push("everyone");
152-
groups = groups.filter(
187+
// Filter for duplicates
188+
return all_groups.filter(
153189
(value, index, array) => array.indexOf(value) === index
154190
);
191+
};
155192

156-
// If the only scopes requested are neither profile nor any scope beginning with
157-
// https:// then do not overload with custom claims
158-
const scopes_requested = event.transaction.requested_scopes || [];
159-
160-
let fixup_needed = (scope) => {
161-
return scope === "profile" || scope.startsWith("https://");
193+
const deny = (reason) => {
194+
return {
195+
granted: false,
196+
denied: {
197+
reason,
198+
},
162199
};
200+
};
163201

164-
if (scopes_requested.some(fixup_needed)) {
165-
console.log(
166-
`Client ${event.client.client_id} requested ${scopes_requested}, therefore adding custom claims`
167-
);
168-
api.idToken.setCustomClaim("email_aliases", undefined);
169-
api.idToken.setCustomClaim("dn", undefined);
170-
api.idToken.setCustomClaim("organizationUnits", undefined);
171-
api.idToken.setCustomClaim(`${namespace}/groups`, groups);
172-
173-
const claimMsg =
174-
"Please refer to https://github.com/mozilla-iam/person-api in order to query Mozilla IAM CIS user profile data";
175-
api.idToken.setCustomClaim(`${namespace}/README_FIRST`, claimMsg);
176-
}
177-
178-
//// === Actions don't allow modifying the event.user
179-
//// Update user.groups with new merged values
180-
//user.groups = groups;
181-
202+
// Process the access cache decision
203+
const access_decision = (groups, access_rules, access_file_conf) => {
182204
// This is used for authorized user/groups
183205
let authorized = false;
184-
// Defaut app requested aal to MEDIUM for all apps which do not have this set in access file
206+
207+
// Defaut app requested aal to MEDIUM for all apps which do not have
208+
// this set in access file
185209
let required_aal = "MEDIUM";
186210

187-
for (let i = 0; i < access_rules.length; i++) {
188-
let app = access_rules[i].application;
211+
const apps = access_rules.filter(
212+
(a) =>
213+
(a.application.client_id ?? "").indexOf(event.client.client_id) >= 0
214+
);
215+
216+
// Default deny for apps we don't define in
217+
// https://github.com/mozilla-iam/sso-dashboard-configuration/blob/master/apps.yml
218+
if (apps.length == 0) {
219+
console.log(`No access rules defined for ${event.client.client_id}`);
220+
return deny("notingroup");
221+
}
222+
223+
// Check users and groups.
224+
for (let i = 0; i < apps.length; i++) {
225+
let app = apps[i].application;
189226

190227
//Handy for quick testing in dev (overrides access rules)
191228
//var app = {'client_id': 'pCGEHXW0VQNrQKURDcGi0tghh7NwWGhW', // This is testrp social-ldap-pwless
@@ -195,19 +232,23 @@ exports.onExecutePostLogin = async (event, api) => {
195232
// };
196233

197234
if (app.client_id && app.client_id.indexOf(event.client.client_id) >= 0) {
198-
// If there are multiple applications in apps.yml with the same client_id
199-
// then this expiration of access check will only run against the first
200-
// one encountered. This matters if there are multiple applications, using
201-
// the same client_id, and asserting different expire_access_when_unused_after
202-
// values.
235+
// If there are multiple applications in apps.yml with the same
236+
// client_id then this expiration of access check will only run
237+
// against the first one encountered. This matters if there are
238+
// multiple applications, using the same client_id, and asserting
239+
// different expire_access_when_unused_after values.
203240

204241
// Set app AAL (AA level) if present
205242
required_aal = app.AAL || required_aal;
206243

207244
// AUTHORIZED_{GROUPS,USERS}
208-
// XXX this authorized_users SHOULD BE REMOVED as it's unsafe (too easy to make mistakes). USE GROUPS.
209-
// XXX This needs to be fixed in the dashboard first
210-
// Empty users or groups (length == 0) means no access in the dashboard apps.yml world
245+
//
246+
// XXX this authorized_users SHOULD BE REMOVED as it's unsafe (too
247+
// easy to make mistakes). USE GROUPS.
248+
//
249+
// XXX This needs to be fixed in the dashboard first. Empty users
250+
// or groups (length == 0) means no access in the dashboard
251+
// apps.yml world.
211252
if (
212253
app.authorized_users.length === 0 &&
213254
app.authorized_groups.length === 0
@@ -216,7 +257,7 @@ exports.onExecutePostLogin = async (event, api) => {
216257
`Access denied to ${event.client.client_id} for user ${event.user.email} (${event.user.user_id})` +
217258
` - this app denies ALL users and ALL groups")`;
218259
console.log(msg);
219-
return "notingroup";
260+
return deny("notingroup");
220261
}
221262

222263
// Check if the user is authorized to access
@@ -241,7 +282,7 @@ exports.onExecutePostLogin = async (event, api) => {
241282
`Access denied to ${event.client.client_id} for user ${event.user.email} (${event.user.user_id})` +
242283
` - not in authorized group or not an authorized user`;
243284
console.log(msg);
244-
return "notingroup";
285+
return deny("notingroup");
245286
}
246287
} // correct client id / we matched the current RP
247288
} // for loop / next rule in apps.yml
@@ -252,22 +293,10 @@ exports.onExecutePostLogin = async (event, api) => {
252293
// We go through each possible attribute as auth0 will translate these differently in the main profile
253294
// depending on the connection type
254295

255-
const getProfileData = (connection) => {
256-
// Return a single identity by connection name, from the user structure
257-
var i = 0;
258-
for (i = 0; i < event.user.identities.length; i++) {
259-
var cid = event.user.identities[i];
260-
if (cid.connection === connection) {
261-
return cid.profileData;
262-
}
263-
}
264-
265-
return undefined;
266-
}; // getProfileData func
267-
268296
// Ensure all users have some AAI and AAL attributes, even if its empty
269297
let aai = [];
270298
let aal = "UNKNOWN";
299+
let enableDuo = false;
271300

272301
// Allow certain LDAP service accounts to fake their MFA. For all other LDAPi accounts, enforce MFA
273302
if (event.connection.strategy === "ad") {
@@ -277,10 +306,7 @@ exports.onExecutePostLogin = async (event, api) => {
277306
);
278307
aai.push("2FA");
279308
} else {
280-
api.multifactor.enable("duo", {
281-
providerOptions: duoConfig,
282-
allowRememberBrowser: true,
283-
});
309+
enableDuo = true;
284310
console.log(
285311
`duosecurity: ${event.user.email} is in LDAP and requires 2FA check`
286312
);
@@ -391,20 +417,21 @@ exports.onExecutePostLogin = async (event, api) => {
391417
}
392418
}
393419

394-
// Set AAI & AAL claims in idToken
395-
api.idToken.setCustomClaim(`${namespace}/AAI`, aai);
396-
api.idToken.setCustomClaim(`${namespace}/AAL`, aal);
397-
398420
if (!aai_pass) {
399421
const msg =
400422
`Access denied to ${event.client.client_id} for user ${event.user.email} (${event.user.user_id}) - due to` +
401423
` Identity Assurance Level being too low for this RP. Required AAL: ${required_aal} (${aai_pass})`;
402424
console.log(msg);
403-
return "aai_failed";
425+
return deny("aai_failed");
404426
}
405427

406428
// We matched no rule, access is granted
407-
return true;
429+
return {
430+
granted: true,
431+
enableDuo,
432+
aai,
433+
aal,
434+
};
408435
};
409436

410437
const access_file_conf = {
@@ -433,15 +460,25 @@ exports.onExecutePostLogin = async (event, api) => {
433460
try {
434461
const cdnUrl = "https://cdn.sso.mozilla.com/apps.yml";
435462
const appsYaml = await getAppsYaml(cdnUrl);
436-
const decision = access_decision(appsYaml, access_file_conf);
463+
const groups = groupsGather();
464+
const decision = access_decision(groups, appsYaml, access_file_conf);
437465

438-
if (decision === true) {
439-
return; // Allow login to continue
440-
} else {
441-
// Go back to the shadow. You shall not pass!
442-
postError(decision);
466+
if (decision.granted) {
467+
if (decision.enableDuo) {
468+
api.multifactor.enable("duo", {
469+
providerOptions: duoConfig,
470+
allowRememberBrowser: true,
471+
});
472+
}
473+
// Set groups, AAI, and AAL claims in idToken
474+
api.idToken.setCustomClaim(`${namespace}/AAI`, decision.aai);
475+
api.idToken.setCustomClaim(`${namespace}/AAL`, decision.aal);
476+
groupsSetCustomClaims(groups);
443477
return;
444478
}
479+
480+
// Go back to the shadow. You shall not pass!
481+
return postError(decision.denied.reason);
445482
} catch (err) {
446483
// All error should be caught here and we return the callback handler with the error
447484
console.log("AccessRules:", err);

tf/tests/accessRules.test.js

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -95,14 +95,16 @@ test("Connection is not LDAP, do not call api.multifactor.enable", async () => {
9595
});
9696

9797
test("user in LDAP (ad) requires 2FA", async () => {
98-
(_event.connection = {
98+
_event.client.client_id = "client00000000000000000000000005";
99+
_event.connection = {
99100
id: "con_qVLhpUZQxluxX5kN",
100101
metadata: {},
101102
name: "Mozilla-LDAP-Dev",
102103
strategy: "ad",
103-
}),
104-
// Execute onExecutePostLogin
105-
await onExecutePostLogin(_event, api);
104+
};
105+
106+
// Execute onExecutePostLogin
107+
await onExecutePostLogin(_event, api);
106108

107109
// Expect api.multifactor.enable to have been called
108110
expect(api.multifactor.enable).toHaveBeenCalled();
@@ -143,6 +145,7 @@ test("email account not verified", async () => {
143145

144146
describe("Test group merges", () => {
145147
test("Expect user.groups to be a merged list of multiple group lists", async () => {
148+
_event.client.client_id = "client00000000000000000000000005";
146149
_event.transaction.requested_scopes = ["email", "profile"];
147150
_event.user.aai = ["2FA"];
148151
_event.user.groups = ["groups_1", "groups_2"];
@@ -179,20 +182,21 @@ describe("Test group merges", () => {
179182
});
180183

181184
describe("Client does not exist in apps.yml", () => {
182-
test("Client id does not exist and aai is 2FA; expect allowed", async () => {
185+
test("Client id does not exist and aai is 2FA; expect not allowed", async () => {
183186
_event.user.multifactor = ["duo"];
184187
await onExecutePostLogin(_event, api);
185-
186-
// expect redirect.url to be undefined
187-
expect(_event.transaction.redirect_uri).toEqual(undefined);
188+
expect(_event.transaction.redirect_uri).toBeDefined();
189+
expect(decodeRedirect(_event.transaction.redirect_uri)).toEqual(
190+
"notingroup"
191+
);
188192
});
189193

190194
test("Client id does not exist and aai is empty; expect not allowed", async () => {
191195
await onExecutePostLogin(_event, api);
192196

193197
expect(_event.transaction.redirect_uri).toBeDefined();
194198
expect(decodeRedirect(_event.transaction.redirect_uri)).toEqual(
195-
"aai_failed"
199+
"notingroup"
196200
);
197201
});
198202
});

tf/tests/modules/apps.yml.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,13 @@ apps:
5555
- everyone
5656
authorized_users: []
5757
client_id: client00000000000000000000000008
58+
- application:
59+
# Tile-only
60+
authorized_groups:
61+
- everyone
62+
- team_moco
63+
- team_mofo
64+
authorized_users: []
5865
`;
5966
return appsYaml;
6067
},

0 commit comments

Comments
 (0)