Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/new-dots-exist.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@redocly/openapi-core': patch
'@redocly/cli': patch
---

Updated the `no-unused-components` rule to validate unused security schemes.
8 changes: 8 additions & 0 deletions .changeset/vast-guests-cry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@redocly/openapi-core': patch
'@redocly/cli': patch
---

Fixed the `remove-unused-components` decorator to remove unused security schemes.

**Warning:** The bundler may now remove more unused components than before.
2 changes: 1 addition & 1 deletion docs/@v2/decorators/remove-unused-components.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Removes unused components from the bundle output.

In this context, "used" means that a component defined in the `components` object is referenced elsewhere in the API document with `$ref`.
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.

## API design principles

Expand Down
2 changes: 1 addition & 1 deletion docs/@v2/rules/oas/no-unused-components.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ slug: /docs/cli/rules/oas/no-unused-components
# no-unused-components

Ensures that every component specified in your API description is used at least once.
In this context, "used" means that a component defined in the `components` object is referenced elsewhere in the API document with `$ref`.
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.

| OAS | Compatibility |
| --- | ------------- |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -245,4 +245,119 @@ describe('oas2 remove-unused-components', () => {
expect(problems[0].ruleId).toEqual('bundler');
expect(problems[0].message).toEqual("Can't resolve $ref");
});

it('should keep securityDefinitions referenced via SecurityRequirement or a $ref', async () => {
const document = parseYamlToDocument(
outdent`
swagger: '2.0'
security:
- api_key: []
paths:
/foo:
get:
security:
- api_key: []
/bar:
get:
security:
- derived: []
securityDefinitions:
api_key:
type: apiKey
name: api_key
in: header
base:
type: apiKey
name: base
in: header
derived:
$ref: '#/securityDefinitions/base'
`,
'foobar.yaml'
);

const results = await bundleDocument({
externalRefResolver: new BaseResolver(),
document,
config: await createConfig({}),
removeUnusedComponents: true,
types: Oas2Types,
});

expect(results.bundle.parsed).toMatchInlineSnapshot(`
{
"paths": {
"/bar": {
"get": {
"security": [
{
"derived": [],
},
],
},
},
"/foo": {
"get": {
"security": [
{
"api_key": [],
},
],
},
},
},
"security": [
{
"api_key": [],
},
],
"securityDefinitions": {
"api_key": {
"in": "header",
"name": "api_key",
"type": "apiKey",
},
"base": {
"in": "header",
"name": "base",
"type": "apiKey",
},
"derived": {
"$ref": "#/securityDefinitions/base",
},
},
"swagger": "2.0",
}
`);
});

it('should remove unused securityDefinitions', async () => {
const document = parseYamlToDocument(
outdent`
swagger: '2.0'
paths: {}
securityDefinitions:
unused:
type: apiKey
name: unused
in: header
`,
'foobar.yaml'
);

const results = await bundleDocument({
externalRefResolver: new BaseResolver(),
document,
config: await createConfig({}),
removeUnusedComponents: true,
types: Oas2Types,
});

expect(results.bundle.parsed).toMatchInlineSnapshot(`
{
"paths": {},
"swagger": "2.0",
}
`);
});
});
15 changes: 15 additions & 0 deletions packages/core/src/decorators/oas2/remove-unused-components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,5 +106,20 @@ export const RemoveUnusedComponents: Oas2Decorator = () => {
registerComponent('securityDefinitions', key.toString());
},
},
SecurityRequirement(requirements) {
for (const schemeName of Object.keys(requirements)) {
// Security requirements reference schemes by name, so we know that this security definition is used in a SecurityRequirement.
const key = `securityDefinitions/${schemeName}`;
const registered = components.get(key);
if (registered) {
registered.usedIn.push('SecurityRequirement');
} else {
components.set(key, {
usedIn: ['SecurityRequirement'],
name: schemeName,
});
}
}
},
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -572,4 +572,116 @@ describe('oas3 remove-unused-components', () => {
}
`);
});

it('should keep securitySchemes referenced via SecurityRequirement or a $ref', async () => {
const document = parseYamlToDocument(
outdent`
openapi: 3.2.0
paths:
/foo:
get:
security:
- api_key: []
/bar:
get:
security:
- derived: []
components:
securitySchemes:
api_key:
type: apiKey
name: api_key
in: header
base:
type: apiKey
name: base
in: header
derived:
$ref: '#/components/securitySchemes/base'
`,
'foobar.yaml'
);

const results = await bundleDocument({
externalRefResolver: new BaseResolver(),
document,
config: await createConfig({}),
removeUnusedComponents: true,
types: Oas3Types,
});

expect(results.bundle.parsed).toMatchInlineSnapshot(`
{
"components": {
"securitySchemes": {
"api_key": {
"in": "header",
"name": "api_key",
"type": "apiKey",
},
"base": {
"in": "header",
"name": "base",
"type": "apiKey",
},
"derived": {
"$ref": "#/components/securitySchemes/base",
},
},
},
"openapi": "3.2.0",
"paths": {
"/bar": {
"get": {
"security": [
{
"derived": [],
},
],
},
},
"/foo": {
"get": {
"security": [
{
"api_key": [],
},
],
},
},
},
}
`);
});

it('should remove unused securitySchemes', async () => {
const document = parseYamlToDocument(
outdent`
openapi: 3.2.0
components:
securitySchemes:
unused:
type: apiKey
name: unused
in: header
derived:
$ref: '#/components/securitySchemes/unused'
`,
'foobar.yaml'
);

const results = await bundleDocument({
externalRefResolver: new BaseResolver(),
document,
config: await createConfig({}),
removeUnusedComponents: true,
types: Oas3Types,
});

expect(results.bundle.parsed).toMatchInlineSnapshot(`
{
"openapi": "3.2.0",
}
`);
});
});
21 changes: 21 additions & 0 deletions packages/core/src/decorators/oas3/remove-unused-components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ export const RemoveUnusedComponents: Oas3Decorator = () => {
'Example',
'RequestBody',
'MediaTypesMap',
'SecurityScheme',
].includes(type.name)
) {
const targetPointer = getComponentKey(ref.$ref);
Expand Down Expand Up @@ -157,5 +158,25 @@ export const RemoveUnusedComponents: Oas3Decorator = () => {
registerComponent('mediaTypes', key.toString());
},
},
NamedSecuritySchemes: {
SecurityScheme(_securityScheme, { key }) {
registerComponent('securitySchemes', key.toString());
},
},
SecurityRequirement(requirements) {
for (const schemeName of Object.keys(requirements)) {
// Security requirements reference security schemes by name, so we know that this security scheme is used in a SecurityRequirement.
const key = `securitySchemes/${schemeName}`;
const registered = components.get(key);
if (registered) {
registered.usedIn.push('SecurityRequirement');
} else {
components.set(key, {
usedIn: ['SecurityRequirement'],
name: schemeName,
});
}
}
},
};
};
Loading
Loading