Skip to content

Commit

Permalink
Add policy definitions and attachments to request context, change pol… (
Browse files Browse the repository at this point in the history
#141)

* Add policy definitions and attachments to request context, change policy executor to use them from context instead of directly from repo

* PR comments
  • Loading branch information
tomeresk authored Jun 22, 2020
1 parent f64ce80 commit 48fa353
Show file tree
Hide file tree
Showing 14 changed files with 99 additions and 119 deletions.
39 changes: 32 additions & 7 deletions services/package-lock.json

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

5 changes: 4 additions & 1 deletion services/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand All @@ -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"
}
}
7 changes: 1 addition & 6 deletions services/src/gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'});
Expand Down
2 changes: 1 addition & 1 deletion services/src/modules/directives/policy/opa.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export async function evaluate(ctx: PolicyExecutionContext): Promise<PolicyExecu

async function getWasmPolicy(ctx: PolicyExecutionContext): Promise<any> {
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);
Expand Down
9 changes: 5 additions & 4 deletions services/src/modules/directives/policy/policy-executor.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -10,8 +10,8 @@ const typeEvaluators = {
};

export class PolicyExecutor {
static repo: ResourceRepository;
private policyDefinitions: PolicyDefinition[];
private policyAttachments: PolicyAttachments;

constructor(
protected policies: Policy[],
Expand All @@ -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() {
Expand All @@ -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}`);
Expand Down
4 changes: 2 additions & 2 deletions services/src/modules/directives/policy/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {PolicyArgsObject, ResourceRepository} from '../../resource-repository/types';
import {PolicyArgsObject, PolicyAttachments} from '../../resource-repository/types';

export type Policy = {
namespace: string;
Expand All @@ -10,7 +10,7 @@ export type Policy = {
export type PolicyExecutionContext = {
namespace: string;
name: string;
repo: ResourceRepository;
policyAttachments: PolicyAttachments;
jwt?: JwtInput;
args?: PolicyArgsObject;
queries?: QueriesResults;
Expand Down
6 changes: 5 additions & 1 deletion services/src/modules/graphqlService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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,
Expand All @@ -106,5 +108,7 @@ const defaultSchema = {
declare module './context' {
interface RequestContext {
authenticationConfig: AuthenticationConfig;
policies: Policy[];
policyAttachments: PolicyAttachments;
}
}
21 changes: 1 addition & 20 deletions services/src/modules/resource-repository/composite.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {ResourceRepository, FetchLatestResult, ResourceGroup} from './types';
import {ResourceRepository, FetchLatestResult} from './types';
import {applyResourceGroupUpdates} from './updates';

export class CompositeResourceRepository implements ResourceRepository {
Expand All @@ -13,30 +13,11 @@ 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<void> {
throw new Error('Multiplexed resource repository cannot handle updates');
}

async writePolicyAttachment(): Promise<void> {
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()));
}
}
37 changes: 5 additions & 32 deletions services/src/modules/resource-repository/fs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {}
Expand All @@ -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<void> {
Expand All @@ -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();

Expand Down
37 changes: 5 additions & 32 deletions services/src/modules/resource-repository/s3.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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) {}
Expand Down Expand Up @@ -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<void> {
await this.config.s3
.putObject({
Expand All @@ -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();

Expand Down
7 changes: 4 additions & 3 deletions services/src/modules/resource-repository/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -70,13 +72,12 @@ export interface FetchLatestResult {
resourceGroup: ResourceGroup;
}

export type PolicyAttachments = {[filename: string]: Buffer};

export interface ResourceRepository {
fetchLatest(): Promise<FetchLatestResult>;
getResourceGroup(): ResourceGroup;
update(rg: ResourceGroup): Promise<void>;
writePolicyAttachment(filename: string, content: Buffer): Promise<void>;
getPolicyAttachment(filename: string): Buffer;
initializePolicyAttachments(): Promise<void>;
}

enum AuthType {
Expand Down
Loading

0 comments on commit 48fa353

Please sign in to comment.