Skip to content

Commit

Permalink
Second iteration
Browse files Browse the repository at this point in the history
  • Loading branch information
AleF83 committed Jun 29, 2020
1 parent cdecf15 commit 3aad83b
Show file tree
Hide file tree
Showing 13 changed files with 159 additions and 51 deletions.
11 changes: 10 additions & 1 deletion services/src/modules/baseSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import GraphQLJSON, {GraphQLJSONObject} from 'graphql-type-json';
import {GraphQLDate, GraphQLDateTime, GraphQLTime} from 'graphql-iso-date';
import {concatAST} from 'graphql';
import {sdl as directivesSdl} from './directives';
import {GraphQLResolverMap} from 'apollo-graphql';

export const baseTypeDef = gql`
scalar JSON
Expand All @@ -11,11 +12,19 @@ export const baseTypeDef = gql`
scalar Date
scalar Time
scalar DateTime
type PolicyResult {
allow: Boolean!
}
type Policy {
default: PolicyResult!
}
`;

export const typeDef = concatAST([baseTypeDef, directivesSdl]);

export const resolvers = {
export const resolvers: GraphQLResolverMap<{}> = {
JSON: GraphQLJSON,
JSONObject: GraphQLJSONObject,
Date: GraphQLDate,
Expand Down
8 changes: 4 additions & 4 deletions services/src/modules/directives/policy/opa.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// @ts-ignore opa-wasm already has TS typings merged, but not yet published on npm
import * as Rego from '@open-policy-agent/opa-wasm';
import {getCompiledFilename} from '../../opaHelper';
import {PolicyExecutionContext, PolicyExecutionResult, QueriesResults} from './types';
import {PolicyExecutionContext, PolicyExecutionResult, QueryResults} from './types';
import {PolicyArgsObject} from '../../resource-repository';

export async function evaluate(ctx: PolicyExecutionContext): Promise<PolicyExecutionResult> {
Expand All @@ -21,16 +21,16 @@ async function getWasmPolicy(ctx: PolicyExecutionContext): Promise<any> {
return rego.load_policy(wasm);
}

function getInput(ctx: PolicyExecutionContext): PolicyInput {
const input: PolicyInput = {};
function getInput(ctx: PolicyExecutionContext): PolicyOpaInput {
const input: PolicyOpaInput = {};

if (ctx.args) input.args = ctx.args;
if (ctx.query) input.query = ctx.query;

return input;
}

type PolicyInput = {
type PolicyOpaInput = {
args?: PolicyArgsObject;
query?: QueryResults;
};
27 changes: 22 additions & 5 deletions services/src/modules/directives/policy/policy-executor.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {GraphQLResolveInfo, graphql} from 'graphql';
import {RequestContext} from '../../context';
import {Policy, GraphQLArguments} from './types';
import {Policy, GraphQLArguments, QueryResults} from './types';
import {Policy as PolicyDefinition, PolicyArgsObject, PolicyAttachments, PolicyQuery} from '../../resource-repository';
import {evaluate as evaluateOpa} from './opa';
import {injectParameters} from '../../paramInjection';
Expand Down Expand Up @@ -28,7 +28,7 @@ export class PolicyExecutor {
await Promise.all(this.policies.map(r => this.validatePolicy(r)));
}

async validatePolicy(policy: Policy) {
async evaluatePolicy(policy: Policy): Promise<boolean> {
const policyDefinition = this.getPolicyDefinition(policy.namespace, policy.name);

const args = policyDefinition.args && this.preparePolicyArgs(policyDefinition.args, policy);
Expand All @@ -44,7 +44,11 @@ export class PolicyExecutor {
policyAttachments: this.policyAttachments,
});
if (!done) throw new Error('in-line query evaluation not yet supported');
return allow || false;
}

async validatePolicy(policy: Policy): Promise<void> {
const allow = await this.evaluatePolicy(policy);
if (!allow) throw new Error(`Unauthorized by policy ${policy.name} in namespace ${policy.namespace}`);
}

Expand Down Expand Up @@ -75,7 +79,10 @@ export class PolicyExecutor {
return policyDefinition;
}

private async evaluatePolicyQuery(query: PolicyQuery, args: PolicyArgsObject = {}): Promise<any> {
private async evaluatePolicyQuery(
query: PolicyQuery,
args: PolicyArgsObject = {}
): Promise<QueryResults | undefined> {
let variableValues =
query.variables &&
Object.entries(query.variables).reduce<{[key: string]: any}>((policyArgs, [varName, varValue]) => {
Expand All @@ -87,7 +94,17 @@ export class PolicyExecutor {
}, {});

// TODO: Run with admin permissions
const gqlResult = await graphql(this.info.schema, query.source, undefined, this.context, variableValues);
return gqlResult.data;
const context: RequestContext = {...this.context, ignorePolicies: true};
const gqlResult = await graphql(this.info.schema, query.gql, undefined, context, variableValues);
return gqlResult.data || undefined;
}
}

declare module '../../context' {
interface RequestContext {
/**
* This flag indicates that request should be resolved without invoking authorization policies evaluation
*/
ignorePolicies: boolean;
}
}
10 changes: 3 additions & 7 deletions services/src/modules/directives/policy/policy-query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,9 @@ export class PolicyQueryDirective extends SchemaDirectiveVisitor {
args: args,
};

const executor = new PolicyExecutor([policy], parent, args, context, info);
try {
await executor.validatePolicies();
return {allow: true};
} catch (error) {
return {allow: false};
}
const executor = new PolicyExecutor([], parent, args, context, info);
const allow = await executor.evaluatePolicy(policy);
return {allow};
};
}
}
Expand Down
6 changes: 4 additions & 2 deletions services/src/modules/directives/policy/policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ export class PolicyDirective extends SchemaDirectiveVisitor {
const policies = this.args.policies;

field.resolve = async (parent: any, args: any, context: RequestContext, info: GraphQLResolveInfo) => {
const executor = new PolicyExecutor(policies, parent, args, context, info);
await executor.validatePolicies();
if (!context.ignorePolicies) {
const executor = new PolicyExecutor(policies, parent, args, context, info);
await executor.validatePolicies();
}

return originalResolve.call(field, parent, args, context, info);
};
Expand Down
32 changes: 22 additions & 10 deletions 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, Policy, PolicyAttachments} from './resource-repository';
import {ResourceGroup, Policy, PolicyAttachments, Schema} from './resource-repository';
import {buildSchemaFromFederatedTypeDefs} from './buildFederatedSchema';
import * as baseSchema from './baseSchema';
import {ActiveDirectoryAuth} from './auth/activeDirectoryAuth';
Expand Down Expand Up @@ -63,13 +63,10 @@ function buildPolicyGqlQuery(policy: Policy): DocumentNode {
: '';

return gql`
type PolicyResult {
allow: Boolean!
extend type Policy {
${policy.metadata.namespace}___${policy.metadata.name}${argStr}: PolicyResult! @policyQuery(namespace: "${policy.metadata.namespace}", name: "${policy.metadata.name}")
}
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 {
Expand All @@ -78,7 +75,7 @@ export function createSchemaConfig(rg: ResourceGroup): GraphQLServiceConfig {
const upstreamClientCredentialsByAuthority = new Map(
rg.upstreamClientCredentials.map(u => [u.activeDirectory.authority, u])
);
const schemas = rg.schemas.length === 0 ? [defaultSchema] : rg.schemas;
const schemas = rg.schemas.length === 0 ? [defaultSchema] : [initialSchema, ...rg.schemas];
const policies = rg.policies ?? [];

const authenticationConfig: AuthenticationConfig = {
Expand All @@ -93,7 +90,7 @@ export function createSchemaConfig(rg: ResourceGroup): GraphQLServiceConfig {

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}`,
`${p.metadata.namespace}___${p.metadata.name}`,
buildPolicyGqlQuery(p),
]);
const schema = buildSchemaFromFederatedTypeDefs({
Expand Down Expand Up @@ -122,11 +119,26 @@ export function createSchemaConfig(rg: ResourceGroup): GraphQLServiceConfig {
};
}

const defaultSchema = {
const defaultSchema: Schema = {
metadata: {namespace: '__internal__', name: 'default'},
schema: 'type Query { default: String! @stub(value: "default") }',
};

const initialSchema: Schema = {
metadata: {
namespace: '__internal__',
name: '__initial__',
},
schema: `
type Query {
policy: Policy! @stub(value: {
default: {
allow: true
}
})
}`,
};

declare module './context' {
interface RequestContext {
authenticationConfig: AuthenticationConfig;
Expand Down
8 changes: 4 additions & 4 deletions services/src/modules/paramInjection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,22 @@ const authzHeaderPrefix = 'Bearer ';

function resolveTemplate(
source: string,
template: string,
key: string,
parent: any,
args: GraphQLArguments,
context: RequestContext,
info: GraphQLResolveInfo
) {
const propPath = template.split('.');
const propPath = key.split('.');
switch (source) {
case 'source':
return parent && R.path(propPath, parent);
case 'args':
return args && R.path(propPath, args);
case 'exports':
return context.exports.resolve(info.parentType, parent, template);
return context.exports.resolve(info.parentType, parent, key);
case 'jwt':
return getJwt(context)[template];
return getJwt(context)[key];
default:
return null;
}
Expand Down
6 changes: 3 additions & 3 deletions services/src/modules/resource-repository/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,11 @@ export interface Policy extends Resource {
}

export interface PolicyQuery {
source: string;
variables?: PolicyQueryGraphqlVariables;
gql: string;
variables?: PolicyQueryVariables;
}

export interface PolicyQueryGraphqlVariables {
export interface PolicyQueryVariables {
[key: string]: any;
}

Expand Down
8 changes: 4 additions & 4 deletions services/src/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {createSchemaConfig} from './modules/graphqlService';
import * as opaHelper from './modules/opaHelper';
// Importing directly from types because of a typescript or ts-jest bug that re-exported enums cause a runtime error for being undefined
// https://github.com/kulshekhar/ts-jest/issues/281
import {PolicyArgsObject, PolicyType} from './modules/resource-repository/types';
import {PolicyArgsObject, PolicyType, PolicyQueryVariables} from './modules/resource-repository/types';

const typeDefs = gql`
scalar JSON
Expand Down Expand Up @@ -109,7 +109,7 @@ const typeDefs = gql`
}
input PolicyQueryInput {
source: String!
gql: String!
variables: JSONObject
}
Expand Down Expand Up @@ -168,8 +168,8 @@ interface UpstreamClientCredentialsInput {
}

interface PolicyQueryInput {
source: string;
variables?: {[name: string]: any};
gql: string;
variables?: PolicyQueryVariables;
}

interface PolicyInput {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,47 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Authorization with queries Query allowed employee 1`] = `
Object {
"data": null,
"errors": Array [
Object {
"extensions": Object {
"code": "INTERNAL_SERVER_ERROR",
"exception": Object {
"stacktrace": Array [
"Error: Unauthorized by policy alwaysDenied in namespace namespace",
" at PolicyExecutor.validatePolicy (/service/dist/modules/directives/policy/policy-executor.js:37: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:14:17)",
],
},
},
"locations": Array [
Object {
"column": 15,
"line": 2,
},
],
"message": "Unauthorized by policy alwaysDenied in namespace namespace",
"path": Array [
"classifiedDepartments",
],
},
],
"extensions": Any<Object>,
"status": 200,
}
`;
exports[`Authorization with queries Query allowed employee 2`] = `
Object {
"address": "Tel Aviv",
"id": "1",
"name": "John Smith",
}
`;
exports[`Authorization with queries Query denied employee 1 1`] = `
Object {
"data": Object {
Expand All @@ -16,10 +58,10 @@ Object {
"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 PolicyExecutor.validatePolicy (/service/dist/modules/directives/policy/policy-executor.js:37: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)",
" at async field.resolve (/service/dist/modules/directives/policy/policy.js:14:17)",
],
},
},
Expand Down Expand Up @@ -57,10 +99,10 @@ Object {
"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 PolicyExecutor.validatePolicy (/service/dist/modules/directives/policy/policy-executor.js:37: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)",
" at async field.resolve (/service/dist/modules/directives/policy/policy.js:14:17)",
],
},
},
Expand Down
Loading

0 comments on commit 3aad83b

Please sign in to comment.