Skip to content

Commit

Permalink
feat(reference): add support for file allow list for FileResolver
Browse files Browse the repository at this point in the history
Refs #2154

BREAKING CHANGE: FileResolver will not detect and process any local file
unless explicitly allowed by fileAllowList option
  • Loading branch information
char0n committed Oct 20, 2022
1 parent 1fdb085 commit 91f7a84
Show file tree
Hide file tree
Showing 13 changed files with 216 additions and 22 deletions.
45 changes: 41 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,12 @@ export class DefaultDefinitionService implements DefinitionService {
);
const dereferenced = await dereferenceApiDOM(node.parent.parent, {
parse: { mediaType, parserOpts: { sourceMap: true } },
resolve: { baseURI: textDocument.uri },
resolve: {
baseURI: textDocument.uri,
resolverOpts: {
fileAllowList: ['*'],
},
},
});
debug(
'definitionService - go to external ref',
Expand Down
7 changes: 6 additions & 1 deletion packages/apidom-ls/src/services/deref/deref-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,12 @@ export class DefaultDerefService implements DerefService {

// dereference
const dereferenced = await dereferenceApiDOM(api, {
resolve: { baseURI },
resolve: {
baseURI,
resolverOpts: {
fileAllowList: ['*'],
},
},
});
const dereferencedValue = toValue(dereferenced);

Expand Down
7 changes: 6 additions & 1 deletion packages/apidom-ls/src/services/hover/hover-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,12 @@ export class DefaultHoverService implements HoverService {
);
const dereferenced = await dereferenceApiDOM(node.parent.parent, {
parse: { mediaType, parserOpts: { sourceMap: true } },
resolve: { baseURI: textDocument.uri },
resolve: {
baseURI: textDocument.uri,
resolverOpts: {
fileAllowList: ['*'],
},
},
});
if (dereferenced) {
debug(
Expand Down
40 changes: 40 additions & 0 deletions packages/apidom-reference/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,46 @@ This resolver plugin is responsible for resolving a local file.
It detects if the provided URI represents a filesystem path and if so,
reads the file and provides its content.

**WARNING**: use this plugin with caution, as it can read files from a local file system.
By default, this plugin will reject to read any files from the local file system, unless
explicitly provided by **fileAllowList** option.

###### Providing file allow list

File allow list can be provided **globally** as an option to `FileResolver` in form
of array of *glob patterns* or *regular expressions*.

```js
import { options, FileResolver, HttpResolverAxios } from '@swagger-api/apidom-reference';

options.resolve.resolvers = [
FileResolver({
fileAllowList: [
'*.json',
/\.json$/,
]
}),
HttpResolverAxios({ timeout: 5000, redirects: 5, withCredentials: false }),
]
```

File allow list can also be provided on ad-hoc basis:

```js
import { resolve } from '@swagger-api/apidom-reference';

await resolve('/home/user/oas.json', {
resolve: {
resolverOpts: {
fileAllowList: [
'*.json',
/\.json$/,
]
},
},
});
```

##### [HttpResolverAxios](https://github.com/swagger-api/apidom/blob/main/packages/apidom-reference/src/resolve/resolvers/HttpResolverAxios.ts)

This resolver plugin is responsible for resolving a remove file represented by HTTP(s) URL.
Expand Down
1 change: 1 addition & 0 deletions packages/apidom-reference/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
"@swagger-api/apidom-parser-adapter-yaml-1-2": "^0.50.0",
"@types/ramda": "=0.28.17",
"axios": "=1.1.3",
"minimatch": "=5.1.0",
"process": "=0.11.10",
"ramda": "=0.28.0",
"ramda-adjunct": "=3.3.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,31 @@
import { readFile } from 'node:fs';
import { promisify } from 'node:util';
import stampit from 'stampit';
import minimatch from 'minimatch';

import { Resolver as IResolver, File as IFile } from '../../../types';
import { FileResolver as IFileResolver, File as IFile } from '../../../types';
import Resolver from '../Resolver';
import * as url from '../../../util/url';
import { ResolverError } from '../../../util/errors';

const FileResolver: stampit.Stamp<IResolver> = stampit(Resolver, {
init() {
this.name = 'file';
const FileResolver: stampit.Stamp<IFileResolver> = stampit(Resolver, {
props: {
name: 'file',
fileAllowList: [],
},
init(this: IFileResolver, { fileAllowList = this.fileAllowList }) {
this.fileAllowList = fileAllowList;
},
methods: {
canRead(file: IFile): boolean {
return url.isFileSystemPath(file.uri);
canRead(this: IFileResolver, file: IFile): boolean {
return (
url.isFileSystemPath(file.uri) &&
this.fileAllowList.some((pattern) => {
return typeof pattern === 'string'
? minimatch(file.uri, pattern, { matchBase: true })
: pattern.test(file.uri);
})
);
},
async read(file: IFile): Promise<Buffer> {
const fileSystemPath = url.toFileSystemPath(file.uri);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ import ResolverError from '../../util/errors/ResolverError';
import { HttpResolver as IHttpResolver, File as IFile } from '../../types';
import HttpResolver from './HttpResolver';

const HttpResolverAxios: stampit.Stamp<IHttpResolver> = stampit(HttpResolver).init(
interface IHttpResolverAxios extends IHttpResolver {
getHttpClient(): AxiosInstance;
}

const HttpResolverAxios: stampit.Stamp<IHttpResolverAxios> = stampit(HttpResolver).init(
function HttpResolverAxios() {
/**
* Private Api.
Expand Down
11 changes: 6 additions & 5 deletions packages/apidom-reference/src/resolve/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,19 @@ import { ResolverError, UnmatchedResolverError } from '../util/errors';
*/
// eslint-disable-next-line import/prefer-default-export
export const readFile = async (file: IFile, options: IReferenceOptions): Promise<Buffer> => {
const resolvers: IResolver[] = await plugins.filter('canRead', file, options.resolve.resolvers);
const optsBoundResolvers: IResolver[] = options.resolve.resolvers.map((resolver) => {
const clonedResolver = Object.create(resolver);
return Object.assign(clonedResolver, options.resolve.resolverOpts);
});

const resolvers: IResolver[] = await plugins.filter('canRead', file, optsBoundResolvers);

// we couldn't find any resolver for this File
if (isEmpty(resolvers)) {
throw new UnmatchedResolverError(file.uri);
}

try {
const optsBoundResolvers = resolvers.map((resolver) => {
const clonedResolver = Object.create(resolver);
return Object.assign(clonedResolver, options.resolve.resolverOpts);
});
const { result } = await plugins.run('read', [file], optsBoundResolvers);
return result;
} catch (error: any) {
Expand Down
7 changes: 6 additions & 1 deletion packages/apidom-reference/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,16 @@ export interface File {
}

export interface Resolver {
type: string;
// name: string; - causing issues with stamps

canRead(file: File): boolean;
read(file: File): Promise<Buffer>;
}

export interface FileResolver extends Resolver {
fileAllowList: (string | RegExp)[];
}

export interface HttpResolver extends Resolver {
timeout: number;
redirects: number;
Expand All @@ -24,6 +28,7 @@ export interface HttpResolver extends Resolver {
}

export interface Parser {
// name: string; - causing issues with stamps
allowEmpty: boolean;
sourceMap: boolean;
fileExtensions: string[];
Expand Down
6 changes: 6 additions & 0 deletions packages/apidom-reference/test/mocha-bootstrap.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,13 @@ const { jestSnapshotPlugin, addSerializer } = require('mocha-chai-jest-snapshot'

const jestApiDOMSerializer = require('../../../scripts/jest-serializer-apidom.cjs');
const jestStringSerializer = require('../../../scripts/jest-serializer-string.cjs');
const { options } = require('../src');

// setup snapshot testing
chai.use(jestSnapshotPlugin());
addSerializer(jestApiDOMSerializer);
addSerializer(jestStringSerializer);

// setup allow list for file resolution
const [fileResolver] = options.resolve.resolvers;
fileResolver.fileAllowList = ['*'];
27 changes: 26 additions & 1 deletion packages/apidom-reference/test/resolve/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import path from 'node:path';
import { assert } from 'chai';
import { mediaTypes } from '@swagger-api/apidom-ns-openapi-3-1';

import { resolve, resolveApiDOM, parse } from '../../src';
import { resolve, resolveApiDOM, parse, FileResolver } from '../../src';
import { UnmatchedResolveStrategyError, ResolverError, ParserError } from '../../src/util/errors';
import OpenApiJson3_1Parser from '../../src/parse/parsers/apidom-reference-parser-openapi-json-3-1';

Expand Down Expand Up @@ -137,6 +137,31 @@ describe('resolve', function () {
});
});

context('given file allow list is provided as resolver option', function () {
specify('should resolve the file', async function () {
const uri = path.join(
__dirname,
'strategies',
'openapi-3-1',
'reference-object',
'fixtures',
'external-indirections',
'root.json',
);
const refSet = await resolve(uri, {
parse: { mediaType: mediaTypes.latest('json') },
resolve: {
resolvers: [FileResolver()],
resolverOpts: {
fileAllowList: ['*'],
},
},
});

assert.strictEqual(refSet.size, 4);
});
});

context("given suitable parser doesn't allow empty files", function () {
specify('should throw error', async function () {
const uri = path.join(__dirname, 'fixtures', 'empty-openapi-3-1-api.json');
Expand Down
Loading

0 comments on commit 91f7a84

Please sign in to comment.