Skip to content

Commit f86720b

Browse files
committed
fix: security handlers 'AND' functionality
1 parent 93e9f6a commit f86720b

12 files changed

+199
-65
lines changed

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -84,3 +84,6 @@ $RECYCLE.BIN/
8484

8585
# Windows shortcuts
8686
*.lnk
87+
88+
# vscode config
89+
.vscode

examples/generated-javascript-project/package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@
2020
},
2121
"devDependencies": {
2222
"fastify": "^4.23.2",
23-
"fastify-cli": "^5.8.0",
23+
"fastify-cli": "^6.0.0",
2424
"tap": "",
25-
"c8": "^8.0.1",
25+
"c8": "^9.0.0",
2626
"@biomejs/biome": "^1.2.2",
2727
"husky": "^8.0.3"
2828
}

examples/generated-standaloneJS-project/index.js

+9-9
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ async function generateRoutes(fastify, opts) {
137137
},
138138
handler: buildHandler(service, "addPet").bind(Service),
139139
prehandler: buildPreHandler(security, [
140-
{ name: "petstore_auth", parameters: ["write:pets", "read:pets"] },
140+
[{ name: "petstore_auth", parameters: ["write:pets", "read:pets"] }],
141141
]).bind(Security),
142142
});
143143

@@ -217,7 +217,7 @@ async function generateRoutes(fastify, opts) {
217217
},
218218
handler: buildHandler(service, "updatePet").bind(Service),
219219
prehandler: buildPreHandler(security, [
220-
{ name: "petstore_auth", parameters: ["write:pets", "read:pets"] },
220+
[{ name: "petstore_auth", parameters: ["write:pets", "read:pets"] }],
221221
]).bind(Security),
222222
});
223223

@@ -244,7 +244,7 @@ async function generateRoutes(fastify, opts) {
244244
},
245245
handler: buildHandler(service, "findPetsByStatus").bind(Service),
246246
prehandler: buildPreHandler(security, [
247-
{ name: "petstore_auth", parameters: ["write:pets", "read:pets"] },
247+
[{ name: "petstore_auth", parameters: ["write:pets", "read:pets"] }],
248248
]).bind(Security),
249249
});
250250

@@ -269,7 +269,7 @@ async function generateRoutes(fastify, opts) {
269269
},
270270
handler: buildHandler(service, "findPetsByTags").bind(Service),
271271
prehandler: buildPreHandler(security, [
272-
{ name: "petstore_auth", parameters: ["write:pets", "read:pets"] },
272+
[{ name: "petstore_auth", parameters: ["write:pets", "read:pets"] }],
273273
]).bind(Security),
274274
});
275275

@@ -291,7 +291,7 @@ async function generateRoutes(fastify, opts) {
291291
},
292292
handler: buildHandler(service, "getPetById").bind(Service),
293293
prehandler: buildPreHandler(security, [
294-
{ name: "api_key", parameters: [] },
294+
[{ name: "api_key", parameters: [] }],
295295
]).bind(Security),
296296
});
297297

@@ -326,7 +326,7 @@ async function generateRoutes(fastify, opts) {
326326
},
327327
handler: buildHandler(service, "updatePetWithForm").bind(Service),
328328
prehandler: buildPreHandler(security, [
329-
{ name: "petstore_auth", parameters: ["write:pets", "read:pets"] },
329+
[{ name: "petstore_auth", parameters: ["write:pets", "read:pets"] }],
330330
]).bind(Security),
331331
});
332332

@@ -348,7 +348,7 @@ async function generateRoutes(fastify, opts) {
348348
},
349349
handler: buildHandler(service, "deletePet").bind(Service),
350350
prehandler: buildPreHandler(security, [
351-
{ name: "petstore_auth", parameters: ["write:pets", "read:pets"] },
351+
[{ name: "petstore_auth", parameters: ["write:pets", "read:pets"] }],
352352
]).bind(Security),
353353
});
354354

@@ -383,7 +383,7 @@ async function generateRoutes(fastify, opts) {
383383
},
384384
handler: buildHandler(service, "uploadFile").bind(Service),
385385
prehandler: buildPreHandler(security, [
386-
{ name: "petstore_auth", parameters: ["write:pets", "read:pets"] },
386+
[{ name: "petstore_auth", parameters: ["write:pets", "read:pets"] }],
387387
]).bind(Security),
388388
});
389389

@@ -393,7 +393,7 @@ async function generateRoutes(fastify, opts) {
393393
schema: {},
394394
handler: buildHandler(service, "getInventory").bind(Service),
395395
prehandler: buildPreHandler(security, [
396-
{ name: "api_key", parameters: [] },
396+
[{ name: "api_key", parameters: [] }],
397397
]).bind(Security),
398398
});
399399

examples/generated-standaloneJS-project/package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@
1919
},
2020
"devDependencies": {
2121
"fastify": "^4.23.2",
22-
"fastify-cli": "^5.8.0",
22+
"fastify-cli": "^6.0.0",
2323
"tap": "",
24-
"c8": "^8.0.1",
24+
"c8": "^9.0.0",
2525
"@biomejs/biome": "^1.2.2",
2626
"husky": "^8.0.3"
2727
}

examples/generated-standaloneJS-project/securityHandlers.js

+27-21
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@ export default class SecurityHandlers {
1313
/** constructor */
1414
constructor(handlers) {
1515
this.handlers = handlers;
16-
// the specificatio allows for an empty scheme that allows all access
17-
this.handlers[emptyScheme] = async () => {};
1816
this.handlerMap = new Map();
1917
this.missingHandlers = [];
2018
}
@@ -25,21 +23,23 @@ export default class SecurityHandlers {
2523
}
2624
const mapKey = JSON.stringify(schemes);
2725
if (!this.handlerMap.has(mapKey)) {
28-
const processedSchemes = [];
29-
for (const scheme of schemes) {
30-
// parser returns undefined on empty scheme
31-
if (scheme.name === undefined) {
32-
scheme.name = emptyScheme;
33-
}
34-
if (!(scheme.name in this.handlers)) {
35-
this.handlers[scheme.name] = () => {
36-
throw `Missing handler for "${scheme.name}" validation`;
37-
};
38-
this.missingHandlers.push(scheme.name);
26+
const processedSchemesList = [];
27+
for (const schemeList of schemes) {
28+
const processedSchemes = [];
29+
if (schemeList?.length > 0) {
30+
for (const scheme of schemeList) {
31+
if (!(scheme.name in this.handlers)) {
32+
this.handlers[scheme.name] = () => {
33+
throw `Missing handler for "${scheme.name}" validation`;
34+
};
35+
this.missingHandlers.push(scheme.name);
36+
}
37+
processedSchemes.push(scheme);
38+
}
3939
}
40-
processedSchemes.push(scheme);
40+
processedSchemesList.push(processedSchemes);
4141
}
42-
this.handlerMap.set(mapKey, this._buildHandler(processedSchemes));
42+
this.handlerMap.set(mapKey, this._buildHandler(processedSchemesList));
4343
}
4444
return this.handlerMap.has(mapKey);
4545
}
@@ -62,24 +62,30 @@ export default class SecurityHandlers {
6262
const securityHandlers = this.handlers;
6363
return async (req, reply) => {
6464
const handlerErrors = [];
65-
const schemeList = [];
65+
const schemeListDone = [];
6666
let statusCode = 401;
67-
for (const scheme of schemes) {
67+
for (const schemeList of schemes) {
68+
let scheme;
69+
const andList = [];
6870
try {
69-
await securityHandlers[scheme.name](req, reply, scheme.parameters);
70-
return; // If one security check passes, no need to try any others
71+
for (scheme of schemeList) {
72+
andList.push(scheme.name);
73+
// all the handlers in a scheme list must succeed
74+
await securityHandlers[scheme.name](req, reply, scheme.parameters);
75+
}
76+
return; // If one list of schemes passes, no need to try any others
7177
} catch (err) {
7278
req.log.debug(`Security handler '${scheme.name}' failed: '${err}'`);
7379
handlerErrors.push(err);
7480
if (err.statusCode !== undefined) {
7581
statusCode = err.statusCode;
7682
}
7783
}
78-
schemeList.push(scheme.name);
84+
schemeListDone.push(andList.toString());
7985
}
8086
// if we get this far no security handlers validated this request
8187
throw new SecurityError(
82-
`None of the security schemes (${schemeList.join(
88+
`None of the security schemes (${schemeListDone.join(
8389
", ",
8490
)}) successfully authenticated this request.`,
8591
statusCode,

lib/ParserBase.js

+8-5
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,14 @@ export class ParserBase {
4747
parseSecurity(schemes) {
4848
return schemes
4949
? schemes.map((item) => {
50-
const name = Object.keys(item)[0];
51-
return {
52-
name,
53-
parameters: item[name],
54-
};
50+
const result = [];
51+
for (const name in item) {
52+
result.push({
53+
name,
54+
parameters: item[name],
55+
});
56+
return result;
57+
}
5558
})
5659
: undefined;
5760
}

lib/securityHandlers.js

+27-21
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@ export default class SecurityHandlers {
1313
/** constructor */
1414
constructor(handlers) {
1515
this.handlers = handlers;
16-
// the specificatio allows for an empty scheme that allows all access
17-
this.handlers[emptyScheme] = async () => {};
1816
this.handlerMap = new Map();
1917
this.missingHandlers = [];
2018
}
@@ -25,21 +23,23 @@ export default class SecurityHandlers {
2523
}
2624
const mapKey = JSON.stringify(schemes);
2725
if (!this.handlerMap.has(mapKey)) {
28-
const processedSchemes = [];
29-
for (const scheme of schemes) {
30-
// parser returns undefined on empty scheme
31-
if (scheme.name === undefined) {
32-
scheme.name = emptyScheme;
33-
}
34-
if (!(scheme.name in this.handlers)) {
35-
this.handlers[scheme.name] = () => {
36-
throw `Missing handler for "${scheme.name}" validation`;
37-
};
38-
this.missingHandlers.push(scheme.name);
26+
const processedSchemesList = [];
27+
for (const schemeList of schemes) {
28+
const processedSchemes = [];
29+
if (schemeList?.length > 0) {
30+
for (const scheme of schemeList) {
31+
if (!(scheme.name in this.handlers)) {
32+
this.handlers[scheme.name] = () => {
33+
throw `Missing handler for "${scheme.name}" validation`;
34+
};
35+
this.missingHandlers.push(scheme.name);
36+
}
37+
processedSchemes.push(scheme);
38+
}
3939
}
40-
processedSchemes.push(scheme);
40+
processedSchemesList.push(processedSchemes);
4141
}
42-
this.handlerMap.set(mapKey, this._buildHandler(processedSchemes));
42+
this.handlerMap.set(mapKey, this._buildHandler(processedSchemesList));
4343
}
4444
return this.handlerMap.has(mapKey);
4545
}
@@ -62,24 +62,30 @@ export default class SecurityHandlers {
6262
const securityHandlers = this.handlers;
6363
return async (req, reply) => {
6464
const handlerErrors = [];
65-
const schemeList = [];
65+
const schemeListDone = [];
6666
let statusCode = 401;
67-
for (const scheme of schemes) {
67+
for (const schemeList of schemes) {
68+
let scheme;
69+
const andList = [];
6870
try {
69-
await securityHandlers[scheme.name](req, reply, scheme.parameters);
70-
return; // If one security check passes, no need to try any others
71+
for (scheme of schemeList) {
72+
andList.push(scheme.name);
73+
// all the handlers in a scheme list must succeed
74+
await securityHandlers[scheme.name](req, reply, scheme.parameters);
75+
}
76+
return; // If one list of schemes passes, no need to try any others
7177
} catch (err) {
7278
req.log.debug(`Security handler '${scheme.name}' failed: '${err}'`);
7379
handlerErrors.push(err);
7480
if (err.statusCode !== undefined) {
7581
statusCode = err.statusCode;
7682
}
7783
}
78-
schemeList.push(scheme.name);
84+
schemeListDone.push(andList.toString());
7985
}
8086
// if we get this far no security handlers validated this request
8187
throw new SecurityError(
82-
`None of the security schemes (${schemeList.join(
88+
`None of the security schemes (${schemeListDone.join(
8389
", ",
8490
)}) successfully authenticated this request.`,
8591
statusCode,

test/service.js

+26
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,32 @@ export class Service {
220220
};
221221
}
222222

223+
// Operation: testOperationSecurity
224+
// summary: Test response serialization
225+
// req.query:
226+
// type: object
227+
// properties:
228+
// respType:
229+
// type: string
230+
//
231+
// valid responses:
232+
// '200':
233+
// description: ok
234+
// schema:
235+
// type: object
236+
// properties:
237+
// response:
238+
// type: string
239+
// required:
240+
// - response
241+
//
242+
243+
async testOperationSecurityUsingAnd(req) {
244+
return {
245+
response: "Authentication succeeded!",
246+
};
247+
}
248+
223249
// Operation: testOperationSecurityWithParameter
224250
// summary: Test response serialization
225251
// req.query:

test/test-openapi.v3.json

+35-1
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,7 @@
267267
"/operationSecurity": {
268268
"get": {
269269
"operationId": "testOperationSecurity",
270-
"summary": "Test security handling",
270+
"summary": "Test security handling OR functionality",
271271
"security": [
272272
{
273273
"api_key": []
@@ -303,6 +303,40 @@
303303
}
304304
}
305305
},
306+
"/operationSecurityUsingAnd": {
307+
"get": {
308+
"operationId": "testOperationSecurityUsingAnd",
309+
"summary": "Test security handling AND functionality",
310+
"security": [
311+
{
312+
"api_key": ["and"],
313+
"skipped": ["works"]
314+
}
315+
],
316+
"responses": {
317+
"200": {
318+
"description": "ok",
319+
"content": {
320+
"application/json": {
321+
"schema": {
322+
"$ref": "#/components/schemas/responseObject"
323+
}
324+
}
325+
}
326+
},
327+
"401": {
328+
"description": "unauthorized",
329+
"content": {
330+
"application/json": {
331+
"schema": {
332+
"$ref": "#/components/schemas/errorObject"
333+
}
334+
}
335+
}
336+
}
337+
}
338+
}
339+
},
306340
"/operationSecurityWithParameter": {
307341
"get": {
308342
"operationId": "testOperationSecurityWithParameter",

0 commit comments

Comments
 (0)