Skip to content

Commit

Permalink
Support for policy query (#143)
Browse files Browse the repository at this point in the history
  • Loading branch information
AleF83 authored Jul 1, 2020
1 parent ea156b0 commit 6dc9682
Show file tree
Hide file tree
Showing 22 changed files with 691 additions and 204 deletions.
66 changes: 34 additions & 32 deletions docs/authorization_spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
Create a new object policy that describes an access logic that can be attached to a graphql field.
Some examples:

### Policy that always allows access:
### Policy that always allows access

```yaml
kind: Policy
Expand All @@ -28,7 +28,7 @@ Spec:
code: {'result': 'allow'}
```
### Policy that allows access only if the issuer is "abc.com":
### Policy that allows access only if the issuer is "abc.com"
```yaml
kind: Policy
Expand Down Expand Up @@ -62,7 +62,7 @@ Spec:
_Note the js-expression type is an example of a possible type and not planned to be implemented at this time._
### Policy that uses a graphql query to fetch additional data and allows based on the results and an argument:
### Policy that uses a graphql query to fetch additional data and allows based on the results and an argument
```yaml
kind: Policy
Expand All @@ -74,29 +74,29 @@ Spec:
code: |
allow = false
allow = {
input.args.userId == input.queries.familyQuery.family.members[_].id
input.args.userId == input.query.user.family.members[_].id
}
args:
userId: ID!
queries:
- type: graphql
name: familyQuery
graphql:
query: |
{
user({jwt.sub}) {
family {
members {
id
}
}
}
sub: ID!
query:
gql: |
query ($sub: String!) {
user(sub: $sub) {
family {
members {
id
}
}
}
}
variables:
sub: '{args.sub}'
```
Queries support the standard Stitch parameter injection syntax, but only support the `jwt` and `args` objects for injection

### Same as the previous policy, but evaluates the query while opa is running:
### Same as the previous policy, but evaluates the query while opa is running

```yaml
kind: Policy
Expand All @@ -118,7 +118,7 @@ Spec:

This example will always evaluate the graphql query, but generally this approach should be used when conditional side effect evaluation is needed

### Policy that uses a policy query and allows based on the results:
### Policy that uses another policy as query and allows based on the results

```yaml
kind: Policy
Expand All @@ -130,24 +130,26 @@ Spec:
code: |
allow = false
allow = {
input.queries.myUserPolicy == true
input.query.myUserPolicy == true
}
args:
userId: ID!
queries:
- type: policy
name: myUserPolicy
policy:
policyName: my-user
args:
userId: '{args.userId}'
query:
gql: |
query(userId: ID!) {
policy.my_user(userId: $userId) {
allow
}
}
variables:
userId: '{args.userId}'
```

`args` for query policy can use parameter injection from the `jwt` and `args` objects, similarly to the graphql query

### Attach policy to fields:

```
```gql
type User {
ID: ID!
Picture: String @policy-some-ns-public
Expand All @@ -163,7 +165,7 @@ Adding args support to policies can ease memoization and remove hidden dependenc

Initially, we will support a single `@policy` directive that gets an array of policy names and their args:

```
```gql
type User {
Picture: String @policy(policies: [
{ namespace: "some-ns", name: “my-user”, args: { userId: “{source.UserId}” } },
Expand All @@ -179,13 +181,13 @@ Later on, for convenience and type safety, we will add an alternative syntax or
1. Client requests a graphql query that includes a field that has a `@policy` directive.
2. Server activates the policy resolver and reads the arguments with parameter injection.
3. The resolver passes the args to a policy executor.
The policy executor invokes the right binding (see policy implementation contract) and manages the queries for the policy.
The policy executor invokes the right binding (see policy implementation contract) and manages the query for the policy.
4. The policy executor should return true/false if the user has access or a string describing the failure.
5. If the user has access, return the field value. Otherwise, return an error.

## Policy implementation contract

The naive js contract can be implemented with a generator, that has a result of true/false and can yield queries.
The naive js contract can be implemented with a generator, that has a result of true/false and can yield query.
All queries and policy evaluation itself are memoized for a request context for same query/policy arguments.

```typescript
Expand Down Expand Up @@ -280,7 +282,7 @@ Spec:

Usage:

```
```gql
type User {
ID: ID!
Picture: String @policy-some-ns-has-claims(claims:["issuer", "sub"], values: ["soluto.com", "{source.UserId}"], jwtClaims: "{jwt.claims}")
Expand Down
18 changes: 18 additions & 0 deletions services/package-lock.json

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

2 changes: 2 additions & 0 deletions services/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"dependencies": {
"@apollo/federation": "^0.12.1",
"@open-policy-agent/opa-wasm": "^1.1.0",
"@types/ramda": "^0.27.6",
"apollo-datasource-rest": "^0.7.0",
"apollo-link-context": "^1.0.19",
"apollo-link-http": "^1.5.16",
Expand All @@ -36,6 +37,7 @@
"p-limit": "^2.2.2",
"pino": "^5.16.0",
"pino-pretty": "^3.6.1",
"ramda": "^0.27.0",
"rxjs": "^6.5.4",
"tslib": "^1.11.0"
},
Expand Down
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
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]);
12 changes: 6 additions & 6 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.queries) input.queries = ctx.queries;
if (ctx.query) input.query = ctx.query;

return input;
}

type PolicyInput = {
type PolicyOpaInput = {
args?: PolicyArgsObject;
queries?: QueriesResults;
query?: QueryResults;
};
Loading

0 comments on commit 6dc9682

Please sign in to comment.