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 29, 2020
1 parent 086c408 commit cdecf15
Show file tree
Hide file tree
Showing 10 changed files with 388 additions and 14 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]);
23 changes: 20 additions & 3 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 @@ -32,7 +32,8 @@ 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}`);
Expand Down Expand Up @@ -73,4 +74,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 @@ -28,6 +28,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 @@ -10,7 +10,7 @@ type jwtData = {
[name: string]: any;
};

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

function resolveTemplate(
Expand Down Expand Up @@ -46,7 +46,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
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 cdecf15

Please sign in to comment.