Skip to content

Commit c6d8235

Browse files
authored
Merge pull request #127 from topcoder-platform/develop
Prod - Updates for design challenge phase shortening / M2M token paths
2 parents b013d4d + b66979d commit c6d8235

9 files changed

Lines changed: 1106 additions & 42 deletions

app-routes.js

Lines changed: 108 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,85 @@ const logger = require("./src/common/logger");
1313
const routes = require("./src/routes");
1414
const authenticator = require("tc-core-library-js").middleware.jwtAuthenticator;
1515

16+
/**
17+
* Returns whether a normalized auth user has interactive roles.
18+
*
19+
* @param {Object} authUser the decoded auth user from the authenticator
20+
* @returns {Boolean} true when the caller has one or more user roles
21+
*/
22+
function hasInteractiveRoles(authUser) {
23+
const roles = _.get(authUser, "roles");
24+
return _.isArray(roles) ? roles.length > 0 : !!roles;
25+
}
26+
27+
/**
28+
* Read the scope claim from an auth user.
29+
*
30+
* The shared authenticator normally copies Auth0's `scope` string into
31+
* `scopes`, but this also handles callers already carrying either shape.
32+
*
33+
* @param {Object} authUser the decoded auth user from the authenticator
34+
* @returns {Array|String|undefined} scopes from the token
35+
*/
36+
function getAuthUserScopes(authUser) {
37+
if (!authUser) {
38+
return undefined;
39+
}
40+
return (
41+
authUser.scopes ||
42+
_.find(authUser, (value, key) => {
43+
return key.indexOf("scope") !== -1;
44+
})
45+
);
46+
}
47+
48+
/**
49+
* Determine whether an auth user represents an M2M caller.
50+
*
51+
* Some valid client-credentials tokens are decoded with scopes but without the
52+
* `isMachine` flag. Treat no-role scoped callers as M2M so route-level scope
53+
* checks, not user group membership, decide access.
54+
*
55+
* @param {Object} authUser the decoded auth user from the authenticator
56+
* @returns {Boolean} true when the caller should be handled as M2M
57+
*/
58+
function isM2MAuthUser(authUser) {
59+
return !!(
60+
authUser &&
61+
(_.get(authUser, "isMachine", false) ||
62+
(getAuthUserScopes(authUser) && !hasInteractiveRoles(authUser)))
63+
);
64+
}
65+
66+
/**
67+
* Check whether an M2M caller has any scope required by the route definition.
68+
*
69+
* @param {Object} def route definition from src/routes.js
70+
* @param {Object} authUser the decoded auth user from the authenticator
71+
* @returns {Boolean} true when one required route scope is present
72+
*/
73+
function hasRequiredM2MScopes(def, authUser) {
74+
const scopes = getAuthUserScopes(authUser);
75+
return !!(def.scopes && scopes && helper.checkIfExists(def.scopes, scopes));
76+
}
77+
78+
/**
79+
* Normalize a valid M2M caller to the shape expected by service authorization.
80+
*
81+
* @param {Object} def route definition from src/routes.js
82+
* @param {Object} authUser the decoded auth user from the authenticator
83+
* @returns {Boolean} true when the caller has required M2M scopes
84+
*/
85+
function normalizeM2MAuthUser(def, authUser) {
86+
if (!isM2MAuthUser(authUser) || !hasRequiredM2MScopes(def, authUser)) {
87+
return false;
88+
}
89+
const scopes = getAuthUserScopes(authUser);
90+
authUser.isMachine = true;
91+
authUser.scopes = _.isString(scopes) ? scopes.split(" ") : scopes;
92+
return true;
93+
}
94+
1695
const sanitizeForLog = (value) => {
1796
const seen = new WeakSet();
1897
try {
@@ -43,7 +122,7 @@ const getSignature = (req) => req.signature || req._reqLogId || "no-signature";
43122
* Configure all routes for express app
44123
* @param app the express app
45124
*/
46-
module.exports = (app) => {
125+
function configureRoutes(app) {
47126
// Load all routes
48127
_.each(routes, (verbs, path) => {
49128
_.each(verbs, (def, verb) => {
@@ -82,17 +161,19 @@ module.exports = (app) => {
82161
});
83162

84163
actions.push((req, res, next) => {
85-
if (req.authUser.isMachine) {
164+
if (isM2MAuthUser(req.authUser)) {
86165
// M2M
87-
if (!req.authUser.scopes || !helper.checkIfExists(def.scopes, req.authUser.scopes)) {
166+
if (!normalizeM2MAuthUser(def, req.authUser)) {
88167
logger.warn(
89168
`[${getSignature(req)}] Machine token scope mismatch. required=${safeInspect(
90169
def.scopes
91-
)} provided=${safeInspect(req.authUser.scopes)}`
170+
)} provided=${safeInspect(getAuthUserScopes(req.authUser))}`
92171
);
93-
next(new errors.ForbiddenError(`You are not allowed to perform this action, because the scopes are incorrect. \
172+
next(
173+
new errors.ForbiddenError(`You are not allowed to perform this action, because the scopes are incorrect. \
94174
Required scopes: ${JSON.stringify(def.scopes)} \
95-
Provided scopes: ${JSON.stringify(req.authUser.scopes)}`));
175+
Provided scopes: ${JSON.stringify(getAuthUserScopes(req.authUser))}`)
176+
);
96177
} else {
97178
req.authUser.handle = config.M2M_AUDIT_HANDLE;
98179
req.authUser.userId = config.M2M_AUDIT_USERID;
@@ -120,9 +201,11 @@ module.exports = (app) => {
120201
def.access
121202
)} provided=${safeInspect(req.authUser.roles)}`
122203
);
123-
next(new errors.ForbiddenError(`You are not allowed to perform this action, because the roles are incorrect. \
204+
next(
205+
new errors.ForbiddenError(`You are not allowed to perform this action, because the roles are incorrect. \
124206
Required roles: ${JSON.stringify(def.access)} \
125-
Provided roles: ${JSON.stringify(req.authUser.roles)}`));
207+
Provided roles: ${JSON.stringify(req.authUser.roles)}`)
208+
);
126209
} else {
127210
// user token is used in create/update challenge to ensure user can create/update challenge under specific project
128211
req.userToken = req.headers.authorization.split(" ")[1];
@@ -135,8 +218,12 @@ module.exports = (app) => {
135218
}
136219
} else {
137220
logger.warn(`[${getSignature(req)}] Authenticated user missing roles`);
138-
next(new errors.ForbiddenError("You are not authorized to perform this action, \
139-
because no roles were provided"));
221+
next(
222+
new errors.ForbiddenError(
223+
"You are not authorized to perform this action, \
224+
because no roles were provided"
225+
)
226+
);
140227
}
141228
}
142229
});
@@ -178,12 +265,8 @@ module.exports = (app) => {
178265
if (!req.authUser) {
179266
logger.info(`[${getSignature(req)}] Public route: no authUser context`);
180267
next();
181-
} else if (req.authUser.isMachine) {
182-
if (
183-
!def.scopes ||
184-
!req.authUser.scopes ||
185-
!helper.checkIfExists(def.scopes, req.authUser.scopes)
186-
) {
268+
} else if (isM2MAuthUser(req.authUser)) {
269+
if (!normalizeM2MAuthUser(def, req.authUser)) {
187270
logger.info(
188271
`[${getSignature(req)}] Public route: preserving machine token whitelist bypass despite scope mismatch`
189272
);
@@ -242,4 +325,13 @@ module.exports = (app) => {
242325
});
243326
}
244327
});
328+
}
329+
330+
module.exports = configureRoutes;
331+
module.exports.__testables = {
332+
getAuthUserScopes,
333+
hasInteractiveRoles,
334+
hasRequiredM2MScopes,
335+
isM2MAuthUser,
336+
normalizeM2MAuthUser,
245337
};

0 commit comments

Comments
 (0)