From d24b5d2ee8c7d0ec5917c5ab76be37e8ee75d9a1 Mon Sep 17 00:00:00 2001 From: Tomer Eskenazi Date: Mon, 22 Jun 2020 16:01:54 +0300 Subject: [PATCH] =?UTF-8?q?Add=20policy=20definitions=20and=20attachments?= =?UTF-8?q?=20to=20request=20context,=20change=20pol=E2=80=A6=20(#141)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add policy definitions and attachments to request context, change policy executor to use them from context instead of directly from repo * PR comments --- services/package-lock.json | 39 +++++++++++++++---- services/package.json | 5 ++- services/src/gateway.ts | 7 +--- services/src/modules/directives/policy/opa.ts | 2 +- .../directives/policy/policy-executor.ts | 9 +++-- .../src/modules/directives/policy/types.ts | 4 +- services/src/modules/graphqlService.ts | 6 ++- .../modules/resource-repository/composite.ts | 21 +--------- .../src/modules/resource-repository/fs.ts | 37 +++--------------- .../src/modules/resource-repository/s3.ts | 37 +++--------------- .../src/modules/resource-repository/types.ts | 7 ++-- .../registry/create-resources.spec.ts | 15 ++++--- .../registry/update-resources.spec.ts | 8 ++-- .../src/tests/integration/resourceBucket.ts | 21 +++++++++- 14 files changed, 99 insertions(+), 119 deletions(-) diff --git a/services/package-lock.json b/services/package-lock.json index 9de3ea5b..65878ca5 100644 --- a/services/package-lock.json +++ b/services/package-lock.json @@ -1156,6 +1156,15 @@ "@types/node": "*" } }, + "@types/xml2js": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.5.tgz", + "integrity": "sha512-yohU3zMn0fkhlape1nxXG2bLEGZRc1FeqF80RoHaYXJN7uibaauXfhzhOJr1Xh36sn+/tx21QAOf07b/xYVk1w==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/yargs": { "version": "15.0.3", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.3.tgz", @@ -1689,6 +1698,20 @@ "version": "3.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" + }, + "xml2js": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", + "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==", + "requires": { + "sax": ">=0.6.0", + "xmlbuilder": "~9.0.1" + } + }, + "xmlbuilder": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", + "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=" } } }, @@ -7751,18 +7774,20 @@ "dev": true }, "xml2js": { - "version": "0.4.19", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", - "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==", + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", + "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", + "dev": true, "requires": { "sax": ">=0.6.0", - "xmlbuilder": "~9.0.1" + "xmlbuilder": "~11.0.0" } }, "xmlbuilder": { - "version": "9.0.7", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", - "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=" + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "dev": true }, "xmlchars": { "version": "2.2.0", diff --git a/services/package.json b/services/package.json index 062f71a9..478d30a1 100644 --- a/services/package.json +++ b/services/package.json @@ -3,6 +3,7 @@ "version": "1.0.0", "scripts": { "test": "jest --config jest.config.js", + "test:debug": "node --inspect-brk node_modules/.bin/jest --runInBand --config jest.config.js", "test:e2e": "jest --config jest.config.e2e.js", "test:full": "jest --config jest.config.full.js", "build": "tsc", @@ -43,6 +44,7 @@ "@types/jest": "^25.1.3", "@types/node": "^13.7.4", "@types/pino": "^5.15.5", + "@types/xml2js": "^0.4.5", "apollo-server-testing": "^2.10.1", "docker-compose": "^0.23.3", "graphql-request": "^1.8.2", @@ -53,6 +55,7 @@ "nock": "^12.0.1", "ts-jest": "^25.2.1", "ts-node-dev": "^1.0.0-pre.44", - "typescript": "^3.7.5" + "typescript": "^3.7.5", + "xml2js": "^0.4.23" } } diff --git a/services/src/gateway.ts b/services/src/gateway.ts index 17e6dce4..bdfb74e6 100644 --- a/services/src/gateway.ts +++ b/services/src/gateway.ts @@ -12,21 +12,16 @@ import { ResourceRepository, CompositeResourceRepository, } from './modules/resource-repository'; -import {PolicyExecutor} from './modules/directives/policy/policy-executor'; async function run() { logger.info('Stitch gateway booting up...'); - const resourceRepository = getResourceRepository(); - const {server, dispose} = createStitchGateway({ - resourceGroups: pollForUpdates(resourceRepository, config.resourceUpdateInterval), + resourceGroups: pollForUpdates(getResourceRepository(), config.resourceUpdateInterval), tracing: config.enableGraphQLTracing, playground: config.enableGraphQLPlayground, introspection: config.enableGraphQLIntrospection, }); - await resourceRepository.initializePolicyAttachments(); - PolicyExecutor.repo = resourceRepository; const app = fastify(); app.register(fastifyMetrics, {endpoint: '/metrics'}); diff --git a/services/src/modules/directives/policy/opa.ts b/services/src/modules/directives/policy/opa.ts index ab51d17f..14ff7a1d 100644 --- a/services/src/modules/directives/policy/opa.ts +++ b/services/src/modules/directives/policy/opa.ts @@ -15,7 +15,7 @@ export async function evaluate(ctx: PolicyExecutionContext): Promise { const filename = getCompiledFilename({namespace: ctx.namespace, name: ctx.name}); - const wasm = ctx.repo.getPolicyAttachment(filename); + const wasm = ctx.policyAttachments[filename]; const rego = new Rego(); return rego.load_policy(wasm); diff --git a/services/src/modules/directives/policy/policy-executor.ts b/services/src/modules/directives/policy/policy-executor.ts index 9ab559b5..ed6a7063 100644 --- a/services/src/modules/directives/policy/policy-executor.ts +++ b/services/src/modules/directives/policy/policy-executor.ts @@ -1,7 +1,7 @@ import {GraphQLResolveInfo} from 'graphql'; import {RequestContext} from '../../context'; import {Policy, GraphQLArguments} from './types'; -import {ResourceRepository, Policy as PolicyDefinition, PolicyArgsObject} from '../../resource-repository'; +import {Policy as PolicyDefinition, PolicyArgsObject, PolicyAttachments} from '../../resource-repository'; import {evaluate as evaluateOpa} from './opa'; import {injectParameters} from '../../paramInjection'; @@ -10,8 +10,8 @@ const typeEvaluators = { }; export class PolicyExecutor { - static repo: ResourceRepository; private policyDefinitions: PolicyDefinition[]; + private policyAttachments: PolicyAttachments; constructor( protected policies: Policy[], @@ -21,7 +21,8 @@ export class PolicyExecutor { protected info: GraphQLResolveInfo ) { // TODO: add jwt data - this.policyDefinitions = PolicyExecutor.repo.getResourceGroup().policies; + this.policyDefinitions = context.policies; + this.policyAttachments = context.policyAttachments; } async validatePolicies() { @@ -37,7 +38,7 @@ export class PolicyExecutor { const evaluate = typeEvaluators[policyDefinition.type]; if (!evaluate) throw new Error(`Unsupported policy type ${policyDefinition.type}`); - const {done, allow} = await evaluate({...policy, args, repo: PolicyExecutor.repo}); + const {done, allow} = await evaluate({...policy, args, policyAttachments: this.policyAttachments}); if (!done) throw new Error('in-line query evaluation not yet supported'); if (!allow) throw new Error(`Unauthorized by policy ${policy.name} in namespace ${policy.namespace}`); diff --git a/services/src/modules/directives/policy/types.ts b/services/src/modules/directives/policy/types.ts index e028565b..48977786 100644 --- a/services/src/modules/directives/policy/types.ts +++ b/services/src/modules/directives/policy/types.ts @@ -1,4 +1,4 @@ -import {PolicyArgsObject, ResourceRepository} from '../../resource-repository/types'; +import {PolicyArgsObject, PolicyAttachments} from '../../resource-repository/types'; export type Policy = { namespace: string; @@ -10,7 +10,7 @@ export type Policy = { export type PolicyExecutionContext = { namespace: string; name: string; - repo: ResourceRepository; + policyAttachments: PolicyAttachments; jwt?: JwtInput; args?: PolicyArgsObject; queries?: QueriesResults; diff --git a/services/src/modules/graphqlService.ts b/services/src/modules/graphqlService.ts index 78afa173..713c3a67 100644 --- a/services/src/modules/graphqlService.ts +++ b/services/src/modules/graphqlService.ts @@ -4,7 +4,7 @@ import {Observable, Subscription} from 'rxjs'; import {shareReplay, map, take, tap, catchError, skip} from 'rxjs/operators'; import {directiveMap} from './directives'; -import {ResourceGroup} from './resource-repository'; +import {ResourceGroup, Policy, PolicyAttachments} from './resource-repository'; import {buildSchemaFromFederatedTypeDefs} from './buildFederatedSchema'; import * as baseSchema from './baseSchema'; import {ActiveDirectoryAuth} from './auth/activeDirectoryAuth'; @@ -86,6 +86,8 @@ export function createSchemaConfig(rg: ResourceGroup): GraphQLServiceConfig { schema, executor(requestContext) { requestContext.context.authenticationConfig = authenticationConfig; + requestContext.context.policies = rg.policies; + requestContext.context.policyAttachments = rg.policyAttachments; return execute({ document: requestContext.document, @@ -106,5 +108,7 @@ const defaultSchema = { declare module './context' { interface RequestContext { authenticationConfig: AuthenticationConfig; + policies: Policy[]; + policyAttachments: PolicyAttachments; } } diff --git a/services/src/modules/resource-repository/composite.ts b/services/src/modules/resource-repository/composite.ts index 21b589b1..cd817478 100644 --- a/services/src/modules/resource-repository/composite.ts +++ b/services/src/modules/resource-repository/composite.ts @@ -1,4 +1,4 @@ -import {ResourceRepository, FetchLatestResult, ResourceGroup} from './types'; +import {ResourceRepository, FetchLatestResult} from './types'; import {applyResourceGroupUpdates} from './updates'; export class CompositeResourceRepository implements ResourceRepository { @@ -13,12 +13,6 @@ export class CompositeResourceRepository implements ResourceRepository { })); } - getResourceGroup(): ResourceGroup { - const rgs = this.repositories.map(r => r.getResourceGroup()); - - return rgs.reduce((rg1, rg2) => applyResourceGroupUpdates(rg1, rg2)); - } - async update(): Promise { throw new Error('Multiplexed resource repository cannot handle updates'); } @@ -26,17 +20,4 @@ export class CompositeResourceRepository implements ResourceRepository { async writePolicyAttachment(): Promise { throw new Error('Multiplexed resource repository cannot handle updates'); } - - public getPolicyAttachment(filename: string): Buffer { - for (const repo of this.repositories) { - const policyAttachment = repo.getPolicyAttachment(filename); - if (policyAttachment) return policyAttachment; - } - - throw new Error(`Policy attachment with the filename ${filename} was not found`); - } - - public async initializePolicyAttachments() { - await Promise.all(this.repositories.map(repo => repo.initializePolicyAttachments())); - } } diff --git a/services/src/modules/resource-repository/fs.ts b/services/src/modules/resource-repository/fs.ts index 37d15a30..1ff6a04d 100644 --- a/services/src/modules/resource-repository/fs.ts +++ b/services/src/modules/resource-repository/fs.ts @@ -3,14 +3,12 @@ import * as envVar from 'env-var'; import {promises as fs} from 'fs'; import * as path from 'path'; import pLimit from 'p-limit'; -import * as config from '../config'; -import logger from '../logger'; -import {FetchLatestResult} from './types'; +import {FetchLatestResult, PolicyAttachments} from './types'; export class FileSystemResourceRepository implements ResourceRepository { protected current?: {mtime: number; rg: ResourceGroup}; protected policyAttachmentsDirInitialized = false; - protected policyAttachments: {[filename: string]: Buffer} = {}; + protected policyAttachments: PolicyAttachments = {}; protected policyAttachmentsRefreshedAt?: Date; constructor(protected pathToFile: string, protected policyAttachmentsFolderPath: string) {} @@ -26,11 +24,10 @@ export class FileSystemResourceRepository implements ResourceRepository { const rg = JSON.parse(contents) as ResourceGroup; this.current = {mtime: stats.mtimeMs, rg}; - return {isNew: true, resourceGroup: rg}; - } + await this.refreshPolicyAttachments(); + rg.policyAttachments = {...this.policyAttachments}; - getResourceGroup(): ResourceGroup { - return this.current!.rg; + return {isNew: true, resourceGroup: rg}; } async update(rg: ResourceGroup): Promise { @@ -44,30 +41,6 @@ export class FileSystemResourceRepository implements ResourceRepository { await fs.writeFile(filePath, content); } - public getPolicyAttachment(filename: string): Buffer { - return this.policyAttachments[filename]; - } - - public async initializePolicyAttachments() { - try { - await this.refreshPolicyAttachments(); - } catch (err) { - logger.fatal({err}, 'Failed fetching fs policy attachments on startup'); - throw err; - } - - setInterval(async () => { - try { - await this.refreshPolicyAttachments(); - } catch (err) { - logger.error( - {err}, - `Failed refreshing fs policy attachments, last successful refresh was at ${this.policyAttachmentsRefreshedAt}` - ); - } - }, config.resourceUpdateInterval); - } - private async refreshPolicyAttachments() { const newRefreshedAt = new Date(); diff --git a/services/src/modules/resource-repository/s3.ts b/services/src/modules/resource-repository/s3.ts index 297f8eb0..198ed5a7 100644 --- a/services/src/modules/resource-repository/s3.ts +++ b/services/src/modules/resource-repository/s3.ts @@ -1,9 +1,7 @@ import * as AWS from 'aws-sdk'; import * as envVar from 'env-var'; import pLimit from 'p-limit'; -import * as config from '../config'; -import logger from '../logger'; -import {ResourceRepository, ResourceGroup, FetchLatestResult} from './types'; +import {ResourceRepository, ResourceGroup, FetchLatestResult, PolicyAttachments} from './types'; interface S3ResourceRepositoryConfig { s3: AWS.S3; @@ -15,7 +13,7 @@ type FileDetails = {filename: string; updatedAt: Date}; export class S3ResourceRepository implements ResourceRepository { protected current?: {etag?: string; rg: ResourceGroup}; - protected policyAttachments: {[filename: string]: Buffer} = {}; + protected policyAttachments: PolicyAttachments = {}; protected policyAttachmentsRefreshedAt?: Date; constructor(protected config: S3ResourceRepositoryConfig) {} @@ -48,16 +46,15 @@ export class S3ResourceRepository implements ResourceRepository { const rg = JSON.parse(bodyRaw) as ResourceGroup; this.current = {etag: response.ETag, rg}; + await this.refreshPolicyAttachments(); + rg.policyAttachments = {...this.policyAttachments}; + return { isNew: true, resourceGroup: rg, }; } - getResourceGroup(): ResourceGroup { - return this.current!.rg; - } - async update(rg: ResourceGroup): Promise { await this.config.s3 .putObject({ @@ -80,30 +77,6 @@ export class S3ResourceRepository implements ResourceRepository { .promise(); } - public getPolicyAttachment(filename: string): Buffer { - return this.policyAttachments[filename]; - } - - public async initializePolicyAttachments() { - try { - await this.refreshPolicyAttachments(); - } catch (err) { - logger.fatal({err}, 'Failed fetching s3 policy attachments on startup'); - throw err; - } - - setInterval(async () => { - try { - await this.refreshPolicyAttachments(); - } catch (err) { - logger.error( - {err}, - `Failed refreshing s3 policy attachments, last successful refresh was at ${this.policyAttachmentsRefreshedAt}` - ); - } - }, config.resourceUpdateInterval); - } - private async refreshPolicyAttachments() { const newRefreshedAt = new Date(); diff --git a/services/src/modules/resource-repository/types.ts b/services/src/modules/resource-repository/types.ts index 3a229b2c..d1dd3b17 100644 --- a/services/src/modules/resource-repository/types.ts +++ b/services/src/modules/resource-repository/types.ts @@ -3,6 +3,8 @@ export interface ResourceGroup { upstreams: Upstream[]; upstreamClientCredentials: UpstreamClientCredentials[]; policies: Policy[]; + // policyAttachments are compiled from the Rego code in opa policies, they are not directly modified by users + policyAttachments?: PolicyAttachments; } export interface Resource { @@ -70,13 +72,12 @@ export interface FetchLatestResult { resourceGroup: ResourceGroup; } +export type PolicyAttachments = {[filename: string]: Buffer}; + export interface ResourceRepository { fetchLatest(): Promise; - getResourceGroup(): ResourceGroup; update(rg: ResourceGroup): Promise; writePolicyAttachment(filename: string, content: Buffer): Promise; - getPolicyAttachment(filename: string): Buffer; - initializePolicyAttachments(): Promise; } enum AuthType { diff --git a/services/src/tests/integration/registry/create-resources.spec.ts b/services/src/tests/integration/registry/create-resources.spec.ts index 319b7519..a92bd13f 100644 --- a/services/src/tests/integration/registry/create-resources.spec.ts +++ b/services/src/tests/integration/registry/create-resources.spec.ts @@ -67,7 +67,12 @@ const policy = { ], }; -const baseResourceGroup = {schemas: [], upstreams: [], upstreamClientCredentials: [], policies: []}; +const baseResourceGroup = { + schemas: [], + upstreams: [], + upstreamClientCredentials: [], + policies: [], +}; describe('Create resource', () => { let client: ApolloServerTestClient; @@ -96,7 +101,7 @@ describe('Create resource', () => { expect(response.errors).toBeUndefined(); expect(response.data).toEqual({updateSchemas: {success: true}}); - expect(bucketContents.current).toEqual({...baseResourceGroup, schemas: [schema]}); + expect(bucketContents.current).toMatchObject({...baseResourceGroup, schemas: [schema]}); }); it('Upstream', async () => { @@ -115,7 +120,7 @@ describe('Create resource', () => { expect(response.errors).toBeUndefined(); expect(response.data).toEqual({updateUpstreams: {success: true}}); - expect(bucketContents.current).toEqual({...baseResourceGroup, upstreams: [upstream]}); + expect(bucketContents.current).toMatchObject({...baseResourceGroup, upstreams: [upstream]}); }); it('UpstreamClientCredentials', async () => { @@ -134,7 +139,7 @@ describe('Create resource', () => { expect(response.errors).toBeUndefined(); expect(response.data).toEqual({updateUpstreamClientCredentials: {success: true}}); - expect(bucketContents.current).toEqual({ + expect(bucketContents.current).toMatchObject({ ...baseResourceGroup, upstreamClientCredentials: [upstreamClientCredentials], }); @@ -158,7 +163,7 @@ describe('Create resource', () => { expect(response.errors).toBeUndefined(); expect(response.data).toEqual({updatePolicies: {success: true}}); - expect(bucketContents.current).toEqual({...baseResourceGroup, policies: [policy]}); + expect(bucketContents.current).toMatchObject({...baseResourceGroup, policies: [policy]}); const compiledFilename = 'namespace-name.wasm'; const uncompiledPath = path.resolve(tmpPoliciesDir, 'namespace-name.rego'); diff --git a/services/src/tests/integration/registry/update-resources.spec.ts b/services/src/tests/integration/registry/update-resources.spec.ts index f362f30f..ce6007fb 100644 --- a/services/src/tests/integration/registry/update-resources.spec.ts +++ b/services/src/tests/integration/registry/update-resources.spec.ts @@ -102,7 +102,7 @@ describe('Update resource', () => { expect(response.errors).toBeUndefined(); expect(response.data).toEqual({updateSchemas: {success: true}}); - expect(bucketContents.current).toEqual({...baseResourceGroup, schemas: [newSchema]}); + expect(bucketContents.current).toMatchObject({...baseResourceGroup, schemas: [newSchema]}); }); it('Upstream', async () => { @@ -122,7 +122,7 @@ describe('Update resource', () => { expect(response.errors).toBeUndefined(); expect(response.data).toEqual({updateUpstreams: {success: true}}); - expect(bucketContents.current).toEqual({...baseResourceGroup, upstreams: [newUpstream]}); + expect(bucketContents.current).toMatchObject({...baseResourceGroup, upstreams: [newUpstream]}); }); it('UpstreamClientCredentials', async () => { @@ -149,7 +149,7 @@ describe('Update resource', () => { expect(response.errors).toBeUndefined(); expect(response.data).toEqual({updateUpstreamClientCredentials: {success: true}}); - expect(bucketContents.current).toEqual({ + expect(bucketContents.current).toMatchObject({ ...baseResourceGroup, upstreamClientCredentials: [newUpstreamClientCredentials], }); @@ -174,7 +174,7 @@ describe('Update resource', () => { expect(response.errors).toBeUndefined(); expect(response.data).toEqual({updatePolicies: {success: true}}); - expect(bucketContents.current).toEqual({...baseResourceGroup, policies: [newPolicy]}); + expect(bucketContents.current).toMatchObject({...baseResourceGroup, policies: [newPolicy]}); const compiledFilename = 'namespace-name.wasm'; const uncompiledPath = path.resolve(tmpPoliciesDir, 'namespace-name.rego'); diff --git a/services/src/tests/integration/resourceBucket.ts b/services/src/tests/integration/resourceBucket.ts index 273c443a..d4e3c854 100644 --- a/services/src/tests/integration/resourceBucket.ts +++ b/services/src/tests/integration/resourceBucket.ts @@ -1,4 +1,5 @@ import * as nock from 'nock'; +import * as xml2js from 'xml2js'; import {ResourceGroup} from '../../modules/resource-repository'; export function mockResourceBucket(initialValue: ResourceGroup, initialPolicyFiles: MockPolicyFiles = {}) { @@ -6,6 +7,8 @@ export function mockResourceBucket(initialValue: ResourceGroup, initialPolicyFil const bucketName = process.env.S3_RESOURCE_BUCKET_NAME; const objectKey = process.env.S3_RESOURCE_OBJECT_KEY; const policiesKeyPrefix = process.env.S3_POLICY_ATTACHMENTS_KEY_PREFIX; + const policiesPrefixQueryParamRegex = `prefix=${encodeURIComponent(policiesKeyPrefix!)}.*`; + const queryParamsSeparatorRegex = '?.*'; const value = {current: initialValue, policyFiles: initialPolicyFiles}; @@ -17,13 +20,29 @@ export function mockResourceBucket(initialValue: ResourceGroup, initialPolicyFil .reply(200, (_, body) => { value.current = JSON.parse(body as string) as ResourceGroup; }) + .get(new RegExp(`/${bucketName!}${queryParamsSeparatorRegex}${policiesPrefixQueryParamRegex}`)) + .reply(200, () => { + const filenames = Object.keys(value.policyFiles).map(filename => ({ + Key: filename, + LastModified: new Date(), + })); + const xmlBuilder = new xml2js.Builder(); + return xmlBuilder.buildObject({Contents: filenames, IsTruncated: false}); + }) + .get(new RegExp(`/${bucketName!}/${policiesKeyPrefix}.+`)) + .reply(200, uri => { + const filename = getFilenameFromUri(uri); + return {Body: value.policyFiles[filename]}; + }) .put(new RegExp(`/${bucketName!}/${policiesKeyPrefix}.+`)) .reply(200, (uri, body) => { - const filename = uri.split('/').slice(-1)[0]; + const filename = getFilenameFromUri(uri); value.policyFiles[filename] = body as string; }); return value; } +const getFilenameFromUri = (uri: string) => uri.split('/').slice(-1)[0]; + type MockPolicyFiles = {[name: string]: string};