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
9 changes: 9 additions & 0 deletions .changeset/storage-privacy-policy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'oc': patch
'oc-azure-storage-adapter': patch
'oc-gs-storage-adapter': patch
'oc-s3-storage-adapter': patch
'oc-storage-adapters-utils': patch
---

Move OC component privacy classification out of the storage adapters.
18 changes: 8 additions & 10 deletions packages/oc-azure-storage-adapter/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import Cache from 'nice-cache';
import nodeDir, { type PathsResult } from 'node-dir';
import {
getFileInfo,
type IsPrivateFile,
type StorageAdapter,
type StorageAdapterBaseConfig,
strings
Expand Down Expand Up @@ -183,7 +184,11 @@ export default function azureAdapter(conf: AzureConfig): StorageAdapter {
return subDirectories;
};

const putDir = async (dirInput: string, dirOutput: string) => {
const putDir = async (
dirInput: string,
dirOutput: string,
isPrivateFile: IsPrivateFile = () => false
) => {
const paths = await getPaths(dirInput);
const packageJsonFile = path.join(dirInput, 'package.json');
const files = paths.files.filter((file) => file !== packageJsonFile);
Expand All @@ -193,22 +198,15 @@ export default function azureAdapter(conf: AzureConfig): StorageAdapter {
const relativeFile = file.slice(dirInput.length);
const url = (dirOutput + relativeFile).replace(/\\/g, '/');

const serverPattern = /(\\|\/)server\.js/;
const dotFilePattern = /(\\|\/)\..+/;
const privateFilePatterns = [serverPattern, dotFilePattern];
return putFile(
file,
url,
privateFilePatterns.some((r) => r.test(relativeFile))
);
return putFile(file, url, isPrivateFile(relativeFile));
})
);
// Ensuring package.json is uploaded last so we can verify that a component
// was properly uploaded by checking if package.json exists
const packageJsonFileResult = await putFile(
packageJsonFile,
`${dirOutput}/package.json`.replace(/\\/g, '/'),
false
isPrivateFile(packageJsonFile.slice(dirInput.length))
);

return [...filesResults, packageJsonFileResult];
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import adapter from '../src';

test('put directory recognizes server.js and .env to be private', async () => {
test('put directory uses the supplied privacy classifier', async () => {
const client = adapter({
publicContainerName: 'pubcon',
privateContainerName: 'privcon',
Expand All @@ -11,7 +11,9 @@ test('put directory recognizes server.js and .env to be private', async () => {
componentsDir: 'components'
});

const mockResult = (await client.putDir('.', '.')) as Array<{
const mockResult = (await client.putDir('.', '.', (filePath) =>
filePath.endsWith('template.js')
)) as Array<{
fileName: string;
container: string;
}>;
Expand All @@ -20,8 +22,8 @@ test('put directory recognizes server.js and .env to be private', async () => {
const packageMock = mockResult.find((x) => x.fileName === './package.json')!;
const templateMock = mockResult.find((x) => x.fileName === './template.js')!;

expect(serverMock.container).toBe('privcon');
expect(envMock.container).toBe('privcon');
expect(serverMock.container).toBe('pubcon');
expect(envMock.container).toBe('pubcon');
expect(packageMock.container).toBe('pubcon');
expect(templateMock.container).toBe('pubcon');
expect(templateMock.container).toBe('privcon');
});
19 changes: 8 additions & 11 deletions packages/oc-gs-storage-adapter/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import Cache from 'nice-cache';
import nodeDir, { type PathsResult } from 'node-dir';
import {
getFileInfo,
type IsPrivateFile,
type StorageAdapter,
type StorageAdapterBaseConfig,
strings
Expand Down Expand Up @@ -176,7 +177,11 @@ export default function gsAdapter(conf: GsConfig): StorageAdapter {
}
};

const putDir = async (dirInput: string, dirOutput: string) => {
const putDir = async (
dirInput: string,
dirOutput: string,
isPrivateFile: IsPrivateFile = () => false
) => {
const paths = await getPaths(dirInput);
const packageJsonFile = path.join(dirInput, 'package.json');
const files = paths.files.filter((file) => file !== packageJsonFile);
Expand All @@ -187,23 +192,15 @@ export default function gsAdapter(conf: GsConfig): StorageAdapter {
const relativeFile = file.slice(dirInput.length);
const url = (dirOutput + relativeFile).replace(/\\/g, '/');

const serverPattern = /(\\|\/)server\.js/;
const dotFilePattern = /(\\|\/)\..+/;
const privateFilePatterns = [serverPattern, dotFilePattern];
return putFile(
file,
url,
privateFilePatterns.some((r) => r.test(relativeFile)),
client
);
return putFile(file, url, isPrivateFile(relativeFile), client);
})
);
// Ensuring package.json is uploaded last so we can verify that a component
// was properly uploaded by checking if package.json exists
const packageJsonFileResult = await putFile(
packageJsonFile,
`${dirOutput}/package.json`.replace(/\\/g, '/'),
false,
isPrivateFile(packageJsonFile.slice(dirInput.length)),
client
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jest.mock('node-dir', () => {
};
});

test('put directory recognizes server.js and .env to be private', async () => {
test('put directory uses the supplied privacy classifier', async () => {
const options = {
bucket: 'test',
projectId: '12345',
Expand All @@ -29,7 +29,9 @@ test('put directory recognizes server.js and .env to be private', async () => {
};
const client = gs(options);

const mockResult = (await client.putDir('.', '.')) as Array<{
const mockResult = (await client.putDir('.', '.', (filePath) =>
filePath.endsWith('template.js')
)) as Array<{
Key: string;
ACL: string;
}>;
Expand All @@ -38,8 +40,8 @@ test('put directory recognizes server.js and .env to be private', async () => {
const packageMock = mockResult.find((x) => x.Key === './package.json')!;
const templateMock = mockResult.find((x) => x.Key === './template.js')!;

expect(serverMock.ACL).toBe('authenticated-read');
expect(envMock.ACL).toBe('authenticated-read');
expect(serverMock.ACL).toBe('public-read');
expect(envMock.ACL).toBe('public-read');
expect(packageMock.ACL).toBe('public-read');
expect(templateMock.ACL).toBe('public-read');
expect(templateMock.ACL).toBe('authenticated-read');
});
19 changes: 8 additions & 11 deletions packages/oc-s3-storage-adapter/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import nodeDir, { type PathsResult } from 'node-dir';
import {
getFileInfo,
getNextYear,
type IsPrivateFile,
type StorageAdapter,
type StorageAdapterBaseConfig,
strings
Expand Down Expand Up @@ -235,7 +236,11 @@ export default function s3Adapter(conf: S3Config): StorageAdapter {
return result;
};

const putDir = async (dirInput: string, dirOutput: string) => {
const putDir = async (
dirInput: string,
dirOutput: string,
isPrivateFile: IsPrivateFile = () => false
) => {
const paths = await getPaths(dirInput);
const packageJsonFile = path.join(dirInput, 'package.json');
const files = paths.files.filter((file) => file !== packageJsonFile);
Expand All @@ -246,23 +251,15 @@ export default function s3Adapter(conf: S3Config): StorageAdapter {
const relativeFile = file.slice(dirInput.length);
const url = (dirOutput + relativeFile).replace(/\\/g, '/');

const serverPattern = /(\\|\/)server\.js/;
const dotFilePattern = /(\\|\/)\..+/;
const privateFilePatterns = [serverPattern, dotFilePattern];
return putFile(
file,
url,
privateFilePatterns.some((r) => r.test(relativeFile)),
client
);
return putFile(file, url, isPrivateFile(relativeFile), client);
})
);
// Ensuring package.json is uploaded last so we can verify that a component
// was properly uploaded by checking if package.json exists
const packageJsonFileResult = await putFile(
packageJsonFile,
`${dirOutput}/package.json`.replace(/\\/g, '/'),
false,
isPrivateFile(packageJsonFile.slice(dirInput.length)),
client
);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import s3 from '../src';

test('put directory recognizes server.js and .env to be private', async () => {
test('put directory uses the supplied privacy classifier', async () => {
const options = {
bucket: 'test',
region: 'region-test',
Expand All @@ -13,7 +13,9 @@ test('put directory recognizes server.js and .env to be private', async () => {

const client = s3(options);

const mockResult = (await client.putDir('.', '.')) as Array<{
const mockResult = (await client.putDir('.', '.', (filePath) =>
filePath.endsWith('template.js')
)) as Array<{
Key: string;
ACL: string;
}>;
Expand All @@ -22,8 +24,8 @@ test('put directory recognizes server.js and .env to be private', async () => {
const packageMock = mockResult.find((x) => x.Key === './package.json')!;
const templateMock = mockResult.find((x) => x.Key === './template.js')!;

expect(serverMock.ACL).toBe('authenticated-read');
expect(envMock.ACL).toBe('authenticated-read');
expect(serverMock.ACL).toBe('public-read');
expect(envMock.ACL).toBe('public-read');
expect(packageMock.ACL).toBe('public-read');
expect(templateMock.ACL).toBe('public-read');
expect(templateMock.ACL).toBe('authenticated-read');
});
9 changes: 8 additions & 1 deletion packages/oc-storage-adapters-utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,21 @@ export interface StorageAdapterBaseConfig {
refreshInterval?: number;
}

/** Classifies a path relative to the directory passed to `putDir`. */
export type IsPrivateFile = (filePath: string) => boolean;

export interface StorageAdapter {
adapterType: string;
getFile(filePath: string, force?: boolean): Promise<string>;
getJson<T = unknown>(filePath: string, force?: boolean): Promise<T>;
getUrl: (componentName: string, version: string, fileName: string) => string;
listSubDirectories(dir: string): Promise<string[]>;
maxConcurrentRequests: number;
putDir(folderPath: string, filePath: string): Promise<unknown>;
putDir(
folderPath: string,
filePath: string,
isPrivateFile?: IsPrivateFile
): Promise<unknown>;
putFile(
filePath: string,
fileName: string,
Expand Down
10 changes: 7 additions & 3 deletions packages/oc/src/registry/domain/repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ import {
reconcileMetadataFromStorage
} from './metadata-migration';
import registerTemplates from './register-templates';
import getPromiseBasedAdapter from './storage-adapter';
import getPromiseBasedAdapter, {
isPrivateComponentFile
} from './storage-adapter';
import * as validator from './validators';
import * as versionHandler from './version-handler';

Expand Down Expand Up @@ -602,7 +604,8 @@ export default function repository(conf: Config) {
try {
await cdn.putDir(
pkgDetails.outputFolder,
`${options!.componentsDir}/${componentName}/${componentVersion}`
`${options!.componentsDir}/${componentName}/${componentVersion}`,
isPrivateComponentFile
);
await metadataStore.commitVersion(
componentName,
Expand All @@ -622,7 +625,8 @@ export default function repository(conf: Config) {

await cdn.putDir(
pkgDetails.outputFolder,
`${options!.componentsDir}/${componentName}/${componentVersion}`
`${options!.componentsDir}/${componentName}/${componentVersion}`,
isPrivateComponentFile
);

invalidateComponentInfo(componentName, componentVersion);
Expand Down
13 changes: 12 additions & 1 deletion packages/oc/src/registry/domain/storage-adapter.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import type { StorageAdapter } from 'oc-storage-adapters-utils';
import { fromCallback } from 'universalify';

const privateComponentFilePatterns = [/(\\|\/)server\.js/, /(\\|\/)\..+/];

export function isPrivateComponentFile(filePath: string): boolean {
return privateComponentFilePatterns.some((pattern) => pattern.test(filePath));
}

type RemovePromiseOverload<T> = T extends {
(...args: infer B): void;
(...args: any[]): Promise<any>;
Expand Down Expand Up @@ -48,11 +54,16 @@ function isLegacyAdapter(
}

function convertLegacyAdapter(adapter: LegacyStorageAdapter): StorageAdapter {
const putDir = fromCallback(adapter.putDir as any);

return {
getFile: fromCallback(adapter.getFile as any),
getJson: fromCallback(adapter.getJson as any),
listSubDirectories: fromCallback(adapter.listSubDirectories as any),
putDir: fromCallback(adapter.putDir as any),
// Legacy callback adapters use their third argument for the callback and
// already own their directory privacy behaviour.
putDir: (folderPath: string, filePath: string) =>
putDir(folderPath, filePath),
putFile: fromCallback(adapter.putFile as any),
putFileContent: fromCallback(adapter.putFileContent as any),
getUrl: adapter.getUrl,
Expand Down
4 changes: 4 additions & 0 deletions packages/oc/test/unit/registry-domain-repository.js
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,10 @@ describe('registry : domain : repository', () => {
expect(s3Mock.putDir.args[0][1]).to.equal(
'components/hello-world/1.0.1'
);
const isPrivateFile = s3Mock.putDir.args[0][2];
expect(isPrivateFile('/server.js')).to.be.true;
expect(isPrivateFile('/.env')).to.be.true;
expect(isPrivateFile('/template.js')).to.be.false;
});
});

Expand Down
Loading
Loading