@@ -13,6 +13,85 @@ const logger = require("./src/common/logger");
1313const routes = require ( "./src/routes" ) ;
1414const 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+
1695const 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