Skip to content

Commit

Permalink
First working version
Browse files Browse the repository at this point in the history
  • Loading branch information
AleF83 committed Jun 25, 2020
1 parent f0a64c2 commit bf86664
Show file tree
Hide file tree
Showing 11 changed files with 390 additions and 15 deletions.
4 changes: 3 additions & 1 deletion services/src/modules/directives/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {sdl as gqlSdl, GqlDirective} from './gql';
import {sdl as exportSdl, ExportDirective} from './export';
import {sdl as selectSdl, SelectDirective} from './select';
import {sdl as policySdl, PolicyDirective} from './policy/policy';
import {sdl as policyQuerySdl, PolicyQueryDirective} from './policy/policy-query';

export const directiveMap: {[visitorName: string]: typeof SchemaDirectiveVisitor} = {
stub: StubDirective,
Expand All @@ -14,6 +15,7 @@ export const directiveMap: {[visitorName: string]: typeof SchemaDirectiveVisitor
export: ExportDirective,
select: SelectDirective,
policy: PolicyDirective,
policyQuery: PolicyQueryDirective,
};

export const sdl = concatAST([stubSdl, restSdl, gqlSdl, exportSdl, selectSdl, policySdl]);
export const sdl = concatAST([stubSdl, restSdl, gqlSdl, exportSdl, selectSdl, policySdl, policyQuerySdl]);
25 changes: 21 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 {GraphQLResolveInfo, graphql} from 'graphql';
import {RequestContext} from '../../context';
import {Policy, GraphQLArguments} from './types';
import {Policy as PolicyDefinition, PolicyArgsObject, PolicyAttachments} from '../../resource-repository';
import {Policy as PolicyDefinition, PolicyArgsObject, PolicyAttachments, PolicyQuery} from '../../resource-repository';
import {evaluate as evaluateOpa} from './opa';
import {injectParameters} from '../../paramInjection';

Expand Down Expand Up @@ -33,12 +33,13 @@ export class PolicyExecutor {
const policyDefinition = this.getPolicyDefinition(policy.namespace, policy.name);

const args = policyDefinition.args && this.preparePolicyArgs(policyDefinition.args, policy);
// TODO: evaluate queries

const query = policyDefinition.query && (await this.evaluatePolicyQuery(policyDefinition.query, args));

const evaluate = typeEvaluators[policyDefinition.type];
if (!evaluate) throw new Error(`Unsupported policy type ${policyDefinition.type}`);

const {done, allow} = await evaluate({...policy, args, policyAttachments: this.policyAttachments});
const {done, allow} = await evaluate({...policy, args, query, 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 Expand Up @@ -70,4 +71,20 @@ export class PolicyExecutor {
if (!policyDefinition) throw new Error(`The policy ${name} in namespace ${namespace} was not found`);
return policyDefinition;
}

private async evaluatePolicyQuery(query: PolicyQuery, args: PolicyArgsObject = {}): Promise<any> {
let variableValues =
query.variables &&
Object.entries(query.variables).reduce<{[key: string]: any}>((policyArgs, [varName, varValue]) => {
if (typeof varValue === 'string') {
varValue = injectParameters(varValue, this.parent, args, this.context, this.info).value;
}
policyArgs[varName] = varValue;
return policyArgs;
}, {});

// TODO: Run with admin permissions
const gqlResult = await graphql(this.info.schema, query.source, undefined, this.context, variableValues);
return gqlResult.data;
}
}
35 changes: 35 additions & 0 deletions services/src/modules/directives/policy/policy-query.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import {SchemaDirectiveVisitor} from 'graphql-tools';
import {GraphQLField, GraphQLResolveInfo} from 'graphql';
import {RequestContext} from '../../context';
import {gql} from 'apollo-server-core';
import {PolicyResult, Policy} from './types';
import {PolicyExecutor} from './policy-executor';

export class PolicyQueryDirective extends SchemaDirectiveVisitor {
visitFieldDefinition(field: GraphQLField<any, any>) {
field.resolve = async (
parent: any,
args: any,
context: RequestContext,
info: GraphQLResolveInfo
): Promise<PolicyResult> => {
const policy: Policy = {
namespace: this.args.namespace,
name: this.args.name,
args: args,
};

const executor = new PolicyExecutor([policy], parent, args, context, info);
try {
await executor.validatePolicies();
return {allow: true};
} catch (error) {
return {allow: false};
}
};
}
}

export const sdl = gql`
directive @policyQuery(namespace: String!, name: String!) on FIELD_DEFINITION
`;
4 changes: 4 additions & 0 deletions services/src/modules/directives/policy/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ export type PolicyExecutionResult = {
};
};

export interface PolicyResult {
allow: boolean;
}

export type GraphQLArguments = {
[name: string]: any;
};
30 changes: 26 additions & 4 deletions services/src/modules/graphqlService.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {Unsubscriber, GraphQLServiceConfig, SchemaChangeCallback} from 'apollo-server-core';
import {parse, execute} from 'graphql';
import {Unsubscriber, GraphQLServiceConfig, SchemaChangeCallback, gql} from 'apollo-server-core';
import {parse, execute, DocumentNode} from 'graphql';
import {Observable, Subscription} from 'rxjs';
import {shareReplay, map, take, tap, catchError, skip} from 'rxjs/operators';

Expand Down Expand Up @@ -55,13 +55,31 @@ export function createGraphQLService(config: {resourceGroups: Observable<Resourc
};
}

function buildPolicyGqlQuery(policy: Policy): DocumentNode {
const argStr = policy.args
? `(${Object.entries(policy.args)
.map(([argName, argType]) => `${argName}: ${argType}`)
.join(',')})`
: '';

return gql`
type PolicyResult {
allow: Boolean!
}
type Query {
policy___${policy.metadata.namespace}___${policy.metadata.name}${argStr}: PolicyResult! @policyQuery(namespace: "${policy.metadata.namespace}", name: "${policy.metadata.name}")
}`;
}

export function createSchemaConfig(rg: ResourceGroup): GraphQLServiceConfig {
const activeDirectoryAuth = new ActiveDirectoryAuth();
const upstreamsByHost = new Map(rg.upstreams.map(u => [u.host, u]));
const upstreamClientCredentialsByAuthority = new Map(
rg.upstreamClientCredentials.map(u => [u.activeDirectory.authority, u])
);
const schemas = rg.schemas.length === 0 ? [defaultSchema] : rg.schemas;
const policies = rg.policies ?? [];

const authenticationConfig: AuthenticationConfig = {
getUpstreamByHost(host: string) {
Expand All @@ -73,15 +91,19 @@ export function createSchemaConfig(rg: ResourceGroup): GraphQLServiceConfig {
activeDirectoryAuth,
};

const schemaTypeDefs = schemas.map(s => [`${s.metadata.namespace}/${s.metadata.name}`, parse(s.schema)]);
const policyQueryTypeDefs = policies.map(p => [
`policy___${p.metadata.namespace}___${p.metadata.name}`,
buildPolicyGqlQuery(p),
]);
const schema = buildSchemaFromFederatedTypeDefs({
typeDefs: Object.fromEntries(schemas.map(s => [`${s.metadata.namespace}/${s.metadata.name}`, parse(s.schema)])),
typeDefs: Object.fromEntries([...schemaTypeDefs, ...policyQueryTypeDefs]),
baseTypeDefs: baseSchema.baseTypeDef,
directiveTypeDefs: directivesSdl,
resolvers: baseSchema.resolvers,
schemaDirectives: directiveMap,
schemaDirectivesContext: {authenticationConfig},
});

return {
schema,
executor(requestContext) {
Expand Down
4 changes: 2 additions & 2 deletions services/src/modules/paramInjection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ interface GraphQLArguments {
[key: string]: any;
}

const paramRegex = /{(source|args|exports)\.(\w+(\.\w+)*)}/g;
const paramRegex = /{(source|args|exports)\.(\w+(\.\w+)*)}/;

function resolveTemplate(
source: string,
Expand Down Expand Up @@ -39,7 +39,7 @@ export function injectParameters(
let didFindValues = false;
let didFindTemplates = false;
let value: any = template;
const match = paramRegex.exec(template);
const match = template.match(paramRegex);
if (match) {
value = resolveTemplate(match[1], match[2], parent, args, context, info);
didFindTemplates = true;
Expand Down
2 changes: 1 addition & 1 deletion services/src/modules/resource-repository/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export interface Policy extends Resource {
query?: PolicyQuery;
}

interface PolicyQuery {
export interface PolicyQuery {
source: string;
variables?: PolicyQueryGraphqlVariables;
}
Expand Down
1 change: 1 addition & 0 deletions services/src/tests/e2e/jest-e2e-runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class SerialJestRunner extends DefaultJestRunner {
}

async teardown() {
await dockerCompose.logs(['gateway', 'registry'], {cwd: __dirname, log: true});
await dockerCompose.down({cwd: __dirname, log: true});
await Promise.all([waitFor.gatewayStop(10000), waitFor.registryStop(10000)]);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Authorization with queries Query denied employee 1 1`] = `
Object {
"data": Object {
"deniedEmployee1": Object {
"address": null,
"id": "2",
"name": "Mark Zuckerberg",
},
},
"errors": Array [
Object {
"extensions": Object {
"code": "INTERNAL_SERVER_ERROR",
"exception": Object {
"stacktrace": Array [
"Error: Unauthorized by policy notClassified in namespace namespace",
" at PolicyExecutor.validatePolicy (/service/dist/modules/directives/policy/policy-executor.js:33:19)",
" at async Promise.all (index 0)",
" at async PolicyExecutor.validatePolicies (/service/dist/modules/directives/policy/policy-executor.js:20:9)",
" at async field.resolve (/service/dist/modules/directives/policy/policy.js:13:13)",
],
},
},
"locations": Array [
Object {
"column": 7,
"line": 6,
},
],
"message": "Unauthorized by policy notClassified in namespace namespace",
"path": Array [
"deniedEmployee1",
"address",
],
},
],
"extensions": Any<Object>,
"status": 200,
}
`;
exports[`Authorization with queries Query denied employee 2 1`] = `
Object {
"data": Object {
"deniedEmployee2": Object {
"address": null,
"id": "2",
"name": "Tom Baker",
},
},
"errors": Array [
Object {
"extensions": Object {
"code": "INTERNAL_SERVER_ERROR",
"exception": Object {
"stacktrace": Array [
"Error: Unauthorized by policy notClassified in namespace namespace",
" at PolicyExecutor.validatePolicy (/service/dist/modules/directives/policy/policy-executor.js:33:19)",
" at async Promise.all (index 0)",
" at async PolicyExecutor.validatePolicies (/service/dist/modules/directives/policy/policy-executor.js:20:9)",
" at async field.resolve (/service/dist/modules/directives/policy/policy.js:13:13)",
],
},
},
"locations": Array [
Object {
"column": 7,
"line": 6,
},
],
"message": "Unauthorized by policy notClassified in namespace namespace",
"path": Array [
"deniedEmployee2",
"address",
],
},
],
"extensions": Any<Object>,
"status": 200,
}
`;
Loading

0 comments on commit bf86664

Please sign in to comment.