Skip to content
This repository was archived by the owner on Jul 28, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 6 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
234 changes: 234 additions & 0 deletions custom-plugin-decorators/apply-root-security/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
# Apply root-level security

Authors:

- [`@Daryna-del`](https://github.com/Daryna-del), Daryna Pastushenko (Redocly)

## What this does and why

When you use `redocly join` to combine multiple API descriptions into one, root-level `security` is not automatically inherited across the joined specs. This is by design — silently applying security requirements from one file to operations defined in another would change their behavior without an explicit declaration.

A common scenario is when one spec (for example, `foo.yaml`) defines shared infrastructure — security schemes and root-level `security` — but has no paths of its own, while another spec (`bar.yaml`) defines all the paths but has no `security` at all. After joining, the operations from `bar.yaml` end up with no security applied.

This decorator (`apply-root-security`) solves that: it reads the root-level `security` from a specified source file (for example `foo.yaml`) and sets it as root-level `security` on the document you are bundling when that document does not already define its own. It runs as a `bundle` step, giving you full control over which file supplies the requirement.

Supported spec types and what gets merged:

| Spec | Root security | Security definitions |
| ------------- | ---------------------------------- | ---------------------------------------- |
| OAS3 / OAS3.1 | Merged into `root.security` | Merged into `components.securitySchemes` |
| OAS2 | Merged into `root.security` | Merged into `securityDefinitions` |

## Code

The `security-plugin` plugin defines the `decorator` section and the plugin `id`:

```javascript
export default function plugin() {
return {
id: "security-plugin",
decorators: {
oas3: {'apply-root-security': applyRootSecurity },
oas2: {'apply-root-security': applyRootSecurity },
},
}
}
```

Here's the main part of the decorator (from `decorator.js`):

```javascript
const applyRootSecurity = ({ pathSecurityFile } = {}) => {
return {
Root: {
leave(root, { config, specVersion }) {
const doc = resolvePath(pathSecurityFile, config);

validateOpenapiSpecification(pathSecurityFile, doc, specVersion);

if (specVersion === 'oas2') {
mergeSecurityRequirements(root, doc);
if (doc?.securityDefinitions !== undefined) {
root.securityDefinitions = { ...root.securityDefinitions, ...doc.securityDefinitions };
}
} else {
mergeSecurityRequirements(root, doc);
mergeSecuritySchemes(root, doc);
}
},
},
};
};

```

The `resolvePath` function resolves the path to the security file and returns its parsed content:

```javascript
function resolvePath(pathSecurityFile, config) {
const base = config.configPath ? path.dirname(config.configPath) : process.cwd();
const absolutePath = path.isAbsolute(pathSecurityFile) ? pathSecurityFile : path.resolve(base, pathSecurityFile);
return yaml.load(fs.readFileSync(absolutePath, 'utf8'));
};
```

The `validateOpenapiSpecification` function checks that the security file format matches the target spec version and throws a descriptive error if not — for example, if an OAS2 file is used with an OAS3 target:

```javascript
function validateOpenapiSpecification(pathSecurityFile, doc, specVersion) {
if (specVersion === 'oas2' && doc?.components?.securitySchemes !== undefined && doc?.securityDefinitions === undefined) {
throw new Error(
`apply-root-security: "${pathSecurityFile}" uses OAS3 components.securitySchemes but the target spec is OAS2. Use securityDefinitions instead.`
);
}
if (specVersion !== 'oas2' && doc?.securityDefinitions !== undefined && doc?.components?.securitySchemes === undefined) {
throw new Error(
`apply-root-security: "${pathSecurityFile}" uses OAS2 securityDefinitions but the target spec is ${specVersion}. Use components.securitySchemes instead.`
);
}
};
```

The `mergeSecurityRequirements` function appends root-level security requirements from the source file into the target document. If the target already has security requirements defined, the entries are appended rather than replaced:

```javascript
function mergeSecurityRequirements(root, doc) {
if (!Array.isArray(doc?.security)) return;
root.security = [...(root.security || []), ...doc.security];
};
```

The `mergeSecuritySchemes` function merges the security scheme definitions from the source file into `components.securitySchemes` on the target document. If the target already has schemes defined, they are preserved and the new ones are added alongside them:

```javascript
function mergeSecuritySchemes(root, doc) {
if (doc?.components?.securitySchemes === undefined) return;
if (!root.components) root.components = {};
root.components.securitySchemes = {
...root.components.securitySchemes,
...doc.components.securitySchemes,
};
};
```

Add the following to `redocly.yaml`:

```yaml
plugins:
- './plugin.js'

decorators:
security-plugin/apply-root-security:
pathSecurityFile: ./foo.yaml
```

The `pathSecurityFile` must be in the same format as the spec you are bundling — an OAS3 file for OAS3 targets, an OAS2 file for OAS2 targets.

## Examples

### OAS3

Given two specs:

**foo.yaml** — defines root-level security, no paths:
```yaml
openapi: 3.1.0
info:
title: Foo
version: 1.0.0
security:
- oauth2: []
components:
securitySchemes:
oauth2:
type: oauth2
flows:
authorizationCode:
authorizationUrl: https://example.com/oauth/authorize
tokenUrl: https://example.com/oauth/token
scopes: {}
paths: {}
```

**bar.yaml** — defines paths, no security:
```yaml
openapi: 3.1.0
info:
title: Bar
version: 1.0.0
paths:
/pets:
get:
summary: Get pets example
operationId: getPetsExample
responses:
'200':
description: OK
'400':
description: Bad request
```

Run:

```bash
redocly bundle bar.yaml -o result.yaml
```

`result.yaml` will have `security: [{oauth2: []}]` and `components.securitySchemes.oauth2` applied.

### OAS2

Given two specs:

**foo.yaml** — defines root-level security, no paths:
```yaml
swagger: "2.0"
info:
title: Foo
version: 1.0.0
host: example.com
basePath: /
schemes:
- https
security:
- oauth2: []
securityDefinitions:
oauth2:
type: oauth2
flow: accessCode
authorizationUrl: https://example.com/oauth/authorize
tokenUrl: https://example.com/oauth/token
scopes: {}
paths: {}
```

**bar.yaml** — defines paths, no security:
```yaml
swagger: "2.0"
info:
title: Bar
version: 1.0.0
host: example.com
basePath: /
schemes:
- https
paths:
/pets:
get:
summary: Get pets example
operationId: getPetsExample
responses:
200:
description: OK
400:
description: Bad request
```

`result.yaml` will have `security: [{oauth2: []}]` and `securityDefinitions.oauth2` applied.

## References

- [Redocly join command](https://redocly.com/docs/cli/commands/join)
- [Custom decorators in plugins](https://redocly.com/docs/cli/custom-plugins/custom-decorators)
- [Security requirement object (OpenAPI)](https://spec.openapis.org/oas/v3.1.0#security-requirement-object)
- [Security requirement object (OpenAPI 2 / Swagger)](https://swagger.io/specification/v2/#security-requirement-object)
54 changes: 54 additions & 0 deletions custom-plugin-decorators/apply-root-security/decorator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
const applyRootSecurity = ({ pathSecurityFile } = {}) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const applyRootSecurity = ({ pathSecurityFile } = {}) => {
export const applyRootSecurity = ({ pathSecurityFile } = {}) => {

return {
Root: {
leave(root, { config, specVersion }) {
const doc = resolvePath(pathSecurityFile, config);

validateOpenapiSpecification(pathSecurityFile, doc, specVersion);

if (specVersion === 'oas2') {

@tatomyr tatomyr Jun 12, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's not overcomplicate it. The purpose of the Cookbook is to demonstrate the possibility, not provide the ultimate solution. Let's focus on OAS3 first.

mergeSecurityRequirements(root, doc);
if (doc?.securityDefinitions !== undefined) {
root.securityDefinitions = { ...root.securityDefinitions, ...doc.securityDefinitions };
}
} else {
mergeSecurityRequirements(root, doc);
mergeSecuritySchemes(root, doc);
}
},
},
};
};

function resolvePath(pathSecurityFile, config) {
const base = config.configPath ? path.dirname(config.configPath) : process.cwd();
const absolutePath = path.isAbsolute(pathSecurityFile) ? pathSecurityFile : path.resolve(base, pathSecurityFile);
return yaml.load(fs.readFileSync(absolutePath, 'utf8'));
};

function validateOpenapiSpecification(pathSecurityFile, doc, specVersion) {
if (specVersion === 'oas2' && doc?.components?.securitySchemes !== undefined && doc?.securityDefinitions === undefined) {
throw new Error(
`apply-root-security: "${pathSecurityFile}" uses OAS3 components.securitySchemes but the target spec is OAS2. Use securityDefinitions instead.`
);
}
if (specVersion !== 'oas2' && doc?.securityDefinitions !== undefined && doc?.components?.securitySchemes === undefined) {
throw new Error(
`apply-root-security: "${pathSecurityFile}" uses OAS2 securityDefinitions but the target spec is ${specVersion}. Use components.securitySchemes instead.`
);
}
};

function mergeSecurityRequirements(root, doc) {
if (!Array.isArray(doc?.security)) return;
root.security = [...(root.security || []), ...doc.security];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there's already such a security requirement, will it duplicate it?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, It will duplicate. Added a check for that

};

function mergeSecuritySchemes(root, doc) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you use names like source and targed?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I can. Renamed

if (doc?.components?.securitySchemes === undefined) return;
if (!root.components) root.components = {};
root.components.securitySchemes = {
...root.components.securitySchemes,
...doc.components.securitySchemes,
};
};
11 changes: 11 additions & 0 deletions custom-plugin-decorators/apply-root-security/plugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import applyRootSecurity from "./decorator.js";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
import applyRootSecurity from "./decorator.js";
import { applyRootSecurity } from "./decorator.js";


export default function plugin() {
return {
id: "security-plugin",
decorators: {
oas3: {'apply-root-security': applyRootSecurity },
oas2: {'apply-root-security': applyRootSecurity },
},
}
}
6 changes: 6 additions & 0 deletions custom-plugin-decorators/apply-root-security/redocly.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
plugins:
- './plugin.js'

decorators:
security-plugin/apply-root-security:
pathSecurityFile: ./foo.yaml
Loading