Skip to content
Merged
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
2 changes: 1 addition & 1 deletion docs/guides/upgrading-to-v4.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ Java and Ruby local invocation now fails the command when the local runtime exit

### `plugins` configuration entries are validated

Plugin entries in `serverless.yml` are now validated when osls loads the service. Entries must be lowercase npm package names, scoped npm package names, or explicit local paths beginning with `./` that stay inside the service directory.
Plugin entries in `serverless.yml` are now validated when osls loads the service. Entries must be lowercase npm package names or scoped npm package names, optionally followed by a package subpath such as `@scope/package/lib/plugin`, or explicit local paths beginning with `./` that stay inside the service directory.

Versioned plugin configuration entries such as `example-osls-plugin@1.2.3` now fail with `INVALID_PLUGIN_REFERENCE`; pin plugin versions in `package.json` instead. Non-string entries also fail with `INVALID_PLUGIN_REFERENCE`. Local plugin paths that escape the service directory, such as `./../plugin`, fail with `INVALID_LOCAL_PLUGIN_PATH`.

Expand Down
34 changes: 32 additions & 2 deletions lib/classes/plugin-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,31 @@ const mergeCommands = (target, source) => {
return target;
};

const pluginSubpathSegmentPattern = /^[A-Za-z0-9._~-]+$/;

const splitPluginReference = (reference) => {
const segments = reference.split('/');
const packageSegmentCount = reference.startsWith('@') ? 2 : 1;
return {
packageName: segments.slice(0, packageSegmentCount).join('/'),
subpath:
segments.length > packageSegmentCount ? segments.slice(packageSegmentCount).join('/') : null,
};
};

const validatePluginSubpath = (subpath, entry) => {
const segments = subpath.split('/');
const isValid = segments.every(
(segment) => segment !== '.' && segment !== '..' && pluginSubpathSegmentPattern.test(segment)
);
if (!isValid) {
throw new ServerlessError(
`Invalid plugin reference "${entry}". A package subpath must not be empty or traverse directories.`,
'INVALID_PLUGIN_REFERENCE'
);
}
};

const validateConfiguredPluginReference = (entry, serviceDir) => {
if (typeof entry !== 'string' || entry.trim() !== entry || entry === '') {
throw new ServerlessError('Plugin entries must be strings.', 'INVALID_PLUGIN_REFERENCE');
Expand All @@ -74,7 +99,9 @@ const validateConfiguredPluginReference = (entry, serviceDir) => {
}

const { name, version } = splitPackageSpec(entry);
validatePluginName(name);
const { packageName, subpath } = splitPluginReference(name);
validatePluginName(packageName);
if (subpath != null) validatePluginSubpath(subpath, entry);

if (version != null) {
throw new ServerlessError(
Expand Down Expand Up @@ -253,13 +280,16 @@ class PluginManager {
}

const isLocalPlugin = name.startsWith('./');
const installHint = isLocalPlugin
? ''
: ` Run "serverless plugin install -n ${splitPluginReference(name).packageName}" to install it.`;

throw new ServerlessError(
[
`osls plugin "${name}" not found.`,
' Make sure it\'s installed and listed in the "plugins" section',
' of your serverless config file.',
isLocalPlugin ? '' : ` Run "serverless plugin install -n ${name}" to install it.`,
installHint,
].join(''),
'PLUGIN_NOT_FOUND'
);
Expand Down
26 changes: 26 additions & 0 deletions test/unit/lib/classes/plugin-manager.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -654,6 +654,12 @@ describe('PluginManager', () => {
);
});

it('should suggest installing the package for unknown package subpath plugins', () => {
return expect(pluginManager.loadAllPlugins(['@scope/package/lib/plugin']))
.to.be.eventually.rejected.and.have.property('message')
.that.includes('serverless plugin install -n @scope/package');
});

it('should not throw error when trying to load unknown plugin with help flag', async () => {
const servicePlugins = [servicePluginMock3Name, servicePluginMock1Name];

Expand Down Expand Up @@ -823,6 +829,26 @@ describe('PluginManager', () => {
]);
});

it('preserves package subpath plugin entries', () => {
expect(
pluginManager.parsePluginsObject(['@scope/package/lib/plugin', 'package/plugin']).modules
).to.deep.equal(['@scope/package/lib/plugin', 'package/plugin']);
});

for (const input of ['package/../plugin', '@scope/package/./plugin', 'package//plugin']) {
it(`rejects package subpath plugin entry ${JSON.stringify(input)}`, () => {
expect(() => pluginManager.parsePluginsObject([input]))
.to.throw()
.with.property('code', 'INVALID_PLUGIN_REFERENCE');
});
}

it('rejects versioned package subpath plugin entries', () => {
expect(() => pluginManager.parsePluginsObject(['@scope/package/plugin@1.2.3']))
.to.throw()
.with.property('code', 'INVALID_PLUGIN_REFERENCE');
});

it('rejects non-string plugin entries', () => {
expect(() => pluginManager.parsePluginsObject([{}]))
.to.throw()
Expand Down