refator(backend-common): extract path utilities to plugin api

Signed-off-by: Camila Belo <camilaibs@gmail.com>
This commit is contained in:
Camila Belo
2024-04-30 14:03:56 +02:00
parent a394b66c3e
commit d229dc49ad
47 changed files with 151 additions and 86 deletions
+1
View File
@@ -22,4 +22,5 @@
export * from './services';
export type { BackendFeature } from './types';
export * from './paths';
export * from './wiring';
@@ -0,0 +1,69 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createMockDirectory } from '@backstage/backend-test-utils';
import { resolveSafeChildPath } from './paths';
describe('paths', () => {
describe('resolveSafeChildPath', () => {
const mockDir = createMockDirectory();
const secondDirectory = createMockDirectory();
const workspacePath = mockDir.resolve('workspace');
beforeEach(() => {
mockDir.setContent({
[`${workspacePath}/README.md`]: '### README.md',
});
secondDirectory.setContent({
[`index.md`]: '### index.md',
});
});
it('should throw an error if the path is outside of the base path', () => {
expect(() =>
resolveSafeChildPath(workspacePath, secondDirectory.path),
).toThrow(
'Relative path is not allowed to refer to a directory outside its parent',
);
});
it('should resolve to the full path if the target is inside the directory', () => {
expect(resolveSafeChildPath(workspacePath, './README.md')).toEqual(
`${workspacePath}/README.md`,
);
});
it('should throw an error if the path is a symlink to a directory outside of the base path', () => {
mockDir.addContent({
[`${workspacePath}/symlink`]: ({ symlink }) =>
symlink(secondDirectory.path),
});
expect(() =>
resolveSafeChildPath(workspacePath, './symlink/index.md'),
).toThrow(
'Relative path is not allowed to refer to a directory outside its parent',
);
});
it('should not throw an error when a folder is referenced that doesnt already exist', () => {
expect(resolveSafeChildPath(workspacePath, 'template')).toEqual(
`${workspacePath}/template`,
);
});
});
});
+91
View File
@@ -0,0 +1,91 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { isChildPath } from '@backstage/cli-common';
import { NotAllowedError } from '@backstage/errors';
import { resolve as resolvePath } from 'path';
import { realpathSync as realPath } from 'fs';
/** @internal */
export const packagePathMocks = new Map<
string,
(paths: string[]) => string | undefined
>();
/**
* Resolve a path relative to the root of a package directory.
* Additional path arguments are resolved relative to the package dir.
*
* This is particularly useful when you want to access assets shipped with
* your backend plugin package. When doing so, do not forget to include the assets
* in your published package by adding them to `files` in your `package.json`.
*
* @public
*/
export function resolvePackagePath(name: string, ...paths: string[]) {
const mockedResolve = packagePathMocks.get(name);
if (mockedResolve) {
const resolved = mockedResolve(paths);
if (resolved) {
return resolved;
}
}
const req =
typeof __non_webpack_require__ === 'undefined'
? require
: __non_webpack_require__;
return resolvePath(req.resolve(`${name}/package.json`), '..', ...paths);
}
/**
* Resolves a target path from a base path while guaranteeing that the result is
* a path that point to or within the base path. This is useful for resolving
* paths from user input, as it otherwise opens up for vulnerabilities.
*
* @public
* @param base - The base directory to resolve the path from.
* @param path - The target path, relative or absolute
* @returns A path that is guaranteed to point to or within the base path.
*/
export function resolveSafeChildPath(base: string, path: string): string {
const resolvedBasePath = resolveRealPath(base);
const targetPath = resolvePath(resolvedBasePath, path);
if (!isChildPath(resolvedBasePath, resolveRealPath(targetPath))) {
throw new NotAllowedError(
'Relative path is not allowed to refer to a directory outside its parent',
);
}
// Don't return the resolved path as the original could be a symlink
return resolvePath(base, path);
}
function resolveRealPath(path: string): string {
try {
return realPath(path);
} catch (ex) {
if (ex.code !== 'ENOENT') {
throw ex;
}
}
return path;
}
// Re-export isChildPath so that backend packages don't need to depend on cli-common
export { isChildPath };
+1 -1
View File
@@ -16,7 +16,7 @@
// TODO: Remove this relative import when extrating the path utilities to this package
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { packagePathMocks } from '../../backend-common/src/paths';
import { packagePathMocks } from './paths';
import { posix as posixPath, resolve as resolvePath } from 'path';
/** @public */