Skip to content

Commit 10890ac

Browse files
committed
fix(core): validate/remove unused securitySchemes and securityDefinitions correctly
1 parent 8becb8a commit 10890ac

14 files changed

Lines changed: 529 additions & 11 deletions

File tree

.changeset/new-dots-exist.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@redocly/openapi-core': patch
3+
'@redocly/cli': patch
4+
---
5+
6+
Updated the `no-unused-components` rule to validate unused security schemes.

.changeset/vast-guests-cry.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@redocly/openapi-core': patch
3+
'@redocly/cli': patch
4+
---
5+
6+
Fixed the `remove-unused-components` decorator to remove unused security schemes.
7+
8+
**Warning:** The bundler may now remove more unused components than before.

docs/@v2/decorators/remove-unused-components.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
Removes unused components from the bundle output.
44

5-
In this context, "used" means that a component defined in the `components` object is referenced elsewhere in the API document with `$ref`.
5+
In this context, "used" means that a component is referenced elsewhere in the API document with `$ref`, or that a `securitySchemes` / `securityDefinitions` entry is referenced by name in a `security` requirement object.
66

77
## API design principles
88

docs/@v2/rules/oas/no-unused-components.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ slug: /docs/cli/rules/oas/no-unused-components
55
# no-unused-components
66

77
Ensures that every component specified in your API description is used at least once.
8-
In this context, "used" means that a component defined in the `components` object is referenced elsewhere in the API document with `$ref`.
8+
In this context, "used" means that a component is referenced elsewhere in the API document with `$ref`, or that a `securitySchemes` entry is referenced by name in a `security` requirement object.
99

1010
| OAS | Compatibility |
1111
| --- | ------------- |

packages/core/src/decorators/oas2/__tests__/remove-unused-components.test.ts

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,4 +245,119 @@ describe('oas2 remove-unused-components', () => {
245245
expect(problems[0].ruleId).toEqual('bundler');
246246
expect(problems[0].message).toEqual("Can't resolve $ref");
247247
});
248+
249+
it('should keep securityDefinitions referenced via SecurityRequirement or a $ref', async () => {
250+
const document = parseYamlToDocument(
251+
outdent`
252+
swagger: '2.0'
253+
security:
254+
- api_key: []
255+
paths:
256+
/foo:
257+
get:
258+
security:
259+
- api_key: []
260+
/bar:
261+
get:
262+
security:
263+
- derived: []
264+
securityDefinitions:
265+
api_key:
266+
type: apiKey
267+
name: api_key
268+
in: header
269+
base:
270+
type: apiKey
271+
name: base
272+
in: header
273+
derived:
274+
$ref: '#/securityDefinitions/base'
275+
`,
276+
'foobar.yaml'
277+
);
278+
279+
const results = await bundleDocument({
280+
externalRefResolver: new BaseResolver(),
281+
document,
282+
config: await createConfig({}),
283+
removeUnusedComponents: true,
284+
types: Oas2Types,
285+
});
286+
287+
expect(results.bundle.parsed).toMatchInlineSnapshot(`
288+
{
289+
"paths": {
290+
"/bar": {
291+
"get": {
292+
"security": [
293+
{
294+
"derived": [],
295+
},
296+
],
297+
},
298+
},
299+
"/foo": {
300+
"get": {
301+
"security": [
302+
{
303+
"api_key": [],
304+
},
305+
],
306+
},
307+
},
308+
},
309+
"security": [
310+
{
311+
"api_key": [],
312+
},
313+
],
314+
"securityDefinitions": {
315+
"api_key": {
316+
"in": "header",
317+
"name": "api_key",
318+
"type": "apiKey",
319+
},
320+
"base": {
321+
"in": "header",
322+
"name": "base",
323+
"type": "apiKey",
324+
},
325+
"derived": {
326+
"$ref": "#/securityDefinitions/base",
327+
},
328+
},
329+
"swagger": "2.0",
330+
}
331+
`);
332+
});
333+
334+
it('should remove unused securityDefinitions', async () => {
335+
const document = parseYamlToDocument(
336+
outdent`
337+
swagger: '2.0'
338+
paths: {}
339+
securityDefinitions:
340+
unused:
341+
type: apiKey
342+
name: unused
343+
in: header
344+
`,
345+
'foobar.yaml'
346+
);
347+
348+
const results = await bundleDocument({
349+
externalRefResolver: new BaseResolver(),
350+
document,
351+
config: await createConfig({}),
352+
removeUnusedComponents: true,
353+
types: Oas2Types,
354+
});
355+
356+
expect(results.bundle.parsed).toMatchInlineSnapshot(`
357+
{
358+
"paths": {},
359+
"swagger": "2.0",
360+
}
361+
`);
362+
});
248363
});

packages/core/src/decorators/oas2/remove-unused-components.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,5 +106,20 @@ export const RemoveUnusedComponents: Oas2Decorator = () => {
106106
registerComponent('securityDefinitions', key.toString());
107107
},
108108
},
109+
SecurityRequirement(requirements) {
110+
for (const schemeName of Object.keys(requirements)) {
111+
// Security requirements reference schemes by name, so we know that this security definition is used in a SecurityRequirement.
112+
const key = `securityDefinitions/${schemeName}`;
113+
const registered = components.get(key);
114+
if (registered) {
115+
registered.usedIn.push('SecurityRequirement');
116+
} else {
117+
components.set(key, {
118+
usedIn: ['SecurityRequirement'],
119+
name: schemeName,
120+
});
121+
}
122+
}
123+
},
109124
};
110125
};

packages/core/src/decorators/oas3/__tests__/remove-unused-components.test.ts

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -572,4 +572,116 @@ describe('oas3 remove-unused-components', () => {
572572
}
573573
`);
574574
});
575+
576+
it('should keep securitySchemes referenced via SecurityRequirement or a $ref', async () => {
577+
const document = parseYamlToDocument(
578+
outdent`
579+
openapi: 3.2.0
580+
paths:
581+
/foo:
582+
get:
583+
security:
584+
- api_key: []
585+
/bar:
586+
get:
587+
security:
588+
- derived: []
589+
components:
590+
securitySchemes:
591+
api_key:
592+
type: apiKey
593+
name: api_key
594+
in: header
595+
base:
596+
type: apiKey
597+
name: base
598+
in: header
599+
derived:
600+
$ref: '#/components/securitySchemes/base'
601+
`,
602+
'foobar.yaml'
603+
);
604+
605+
const results = await bundleDocument({
606+
externalRefResolver: new BaseResolver(),
607+
document,
608+
config: await createConfig({}),
609+
removeUnusedComponents: true,
610+
types: Oas3Types,
611+
});
612+
613+
expect(results.bundle.parsed).toMatchInlineSnapshot(`
614+
{
615+
"components": {
616+
"securitySchemes": {
617+
"api_key": {
618+
"in": "header",
619+
"name": "api_key",
620+
"type": "apiKey",
621+
},
622+
"base": {
623+
"in": "header",
624+
"name": "base",
625+
"type": "apiKey",
626+
},
627+
"derived": {
628+
"$ref": "#/components/securitySchemes/base",
629+
},
630+
},
631+
},
632+
"openapi": "3.2.0",
633+
"paths": {
634+
"/bar": {
635+
"get": {
636+
"security": [
637+
{
638+
"derived": [],
639+
},
640+
],
641+
},
642+
},
643+
"/foo": {
644+
"get": {
645+
"security": [
646+
{
647+
"api_key": [],
648+
},
649+
],
650+
},
651+
},
652+
},
653+
}
654+
`);
655+
});
656+
657+
it('should remove unused securitySchemes', async () => {
658+
const document = parseYamlToDocument(
659+
outdent`
660+
openapi: 3.2.0
661+
components:
662+
securitySchemes:
663+
unused:
664+
type: apiKey
665+
name: unused
666+
in: header
667+
derived:
668+
$ref: '#/components/securitySchemes/unused'
669+
`,
670+
'foobar.yaml'
671+
);
672+
673+
const results = await bundleDocument({
674+
externalRefResolver: new BaseResolver(),
675+
document,
676+
config: await createConfig({}),
677+
removeUnusedComponents: true,
678+
types: Oas3Types,
679+
});
680+
681+
expect(results.bundle.parsed).toMatchInlineSnapshot(`
682+
{
683+
"openapi": "3.2.0",
684+
}
685+
`);
686+
});
575687
});

packages/core/src/decorators/oas3/remove-unused-components.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ export const RemoveUnusedComponents: Oas3Decorator = () => {
9090
'Example',
9191
'RequestBody',
9292
'MediaTypesMap',
93+
'SecurityScheme',
9394
].includes(type.name)
9495
) {
9596
const targetPointer = getComponentKey(ref.$ref);
@@ -157,5 +158,25 @@ export const RemoveUnusedComponents: Oas3Decorator = () => {
157158
registerComponent('mediaTypes', key.toString());
158159
},
159160
},
161+
NamedSecuritySchemes: {
162+
SecurityScheme(_securityScheme, { key }) {
163+
registerComponent('securitySchemes', key.toString());
164+
},
165+
},
166+
SecurityRequirement(requirements) {
167+
for (const schemeName of Object.keys(requirements)) {
168+
// Security requirements reference security schemes by name, so we know that this security scheme is used in a SecurityRequirement.
169+
const key = `securitySchemes/${schemeName}`;
170+
const registered = components.get(key);
171+
if (registered) {
172+
registered.usedIn.push('SecurityRequirement');
173+
} else {
174+
components.set(key, {
175+
usedIn: ['SecurityRequirement'],
176+
name: schemeName,
177+
});
178+
}
179+
}
180+
},
160181
};
161182
};

0 commit comments

Comments
 (0)