Skip to content

Authorization PR #39

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 75 commits into
base: authorization-implem
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
75 commits
Select commit Hold shift + click to select a range
11890e3
start branch authorization
tobkle May 21, 2017
f422727
auth code in test/output-app
tobkle May 21, 2017
2ba2f6c
auth end-to-end-test yet without generator
tobkle May 21, 2017
8f86cff
added further authorization tests
tobkle May 28, 2017
ec54b30
reworked and plus authorizedFields()
tobkle Jun 3, 2017
4cb3aa9
User finished before Tweet update
tobkle Jun 4, 2017
95bc453
v0.1
tobkle Jun 4, 2017
392715d
cleanup resolvers
tobkle Jun 4, 2017
087106b
easy auth
tobkle Jun 12, 2017
cd760ae
before simplification
tobkle Jun 14, 2017
8979a86
with authorization folder
tobkle Jun 15, 2017
95edb6f
with authorization folder
tobkle Jun 15, 2017
125617c
removed authorization folder
tobkle Jun 15, 2017
bd45a2c
easier authorization
tobkle Jul 9, 2017
884a8c5
logfile with end-to-end-test
tobkle Jul 9, 2017
e98a412
Pass the logger rather than all the bits it needs
Jul 13, 2017
f621804
Simplify authorized loader.
Jul 13, 2017
151b0b4
Merge pull request #1 from tmeasday/patch-2
tobkle Jul 16, 2017
0e4affa
handle conflicts
tobkle Jul 16, 2017
656c8bf
Merge branch 'tmeasday-patch-1' into authorization
tobkle Jul 16, 2017
e0e6ea9
applied changes
tobkle Jul 16, 2017
957e743
Merge git://github.com/tmeasday/create-graphql-server into authorization
tobkle Jul 16, 2017
9dcdc22
new authorization version from 2017-07-17
tobkle Jul 16, 2017
2e761b1
merged
tobkle Jul 16, 2017
69954ce
merge correction
tobkle Jul 16, 2017
755b224
authorization-simple
tobkle Jul 18, 2017
49cee93
authorization
tobkle Aug 3, 2017
d5a4c32
add-user and authorization prototype
tobkle May 8, 2017
5427807
Revert "add-user and authorization prototype"
tobkle Aug 3, 2017
487277a
Merge branch 'authorization-simple'
tobkle Aug 3, 2017
dc4d1b3
PR adjustments + npm package
tobkle Aug 13, 2017
a1f0e40
harden
tobkle Aug 18, 2017
2e00cc6
prepared for generator
tobkle Aug 18, 2017
fe51626
start branch authorization
tobkle May 21, 2017
d3cc04c
auth code in test/output-app
tobkle May 21, 2017
2119d01
auth end-to-end-test yet without generator
tobkle May 21, 2017
95c1f70
added further authorization tests
tobkle May 28, 2017
53a092e
reworked and plus authorizedFields()
tobkle Jun 3, 2017
835c7db
User finished before Tweet update
tobkle Jun 4, 2017
770ad84
v0.1
tobkle Jun 4, 2017
614e17f
cleanup resolvers
tobkle Jun 4, 2017
807224a
easy auth
tobkle Jun 12, 2017
913aa91
before simplification
tobkle Jun 14, 2017
e458afc
with authorization folder
tobkle Jun 15, 2017
e41e061
with authorization folder
tobkle Jun 15, 2017
757fb43
removed authorization folder
tobkle Jun 15, 2017
aadbe9c
easier authorization
tobkle Jul 9, 2017
f311afd
logfile with end-to-end-test
tobkle Jul 9, 2017
cf68cc4
Pass the logger rather than all the bits it needs
Jul 13, 2017
3188144
Simplify authorized loader.
Jul 13, 2017
66cb5ce
handle conflicts
tobkle Jul 16, 2017
06cfb9b
applied changes
tobkle Jul 16, 2017
0a59e67
new authorization version from 2017-07-17
tobkle Jul 16, 2017
720fbd3
merge correction
tobkle Jul 16, 2017
4f3856b
authorization-simple
tobkle Jul 18, 2017
a39a9ff
authorization
tobkle Aug 3, 2017
4179971
add-user and authorization prototype
tobkle May 8, 2017
c92f598
Revert "add-user and authorization prototype"
tobkle Aug 3, 2017
ab3cb9a
PR adjustments + npm package
tobkle Aug 13, 2017
df1defc
harden
tobkle Aug 18, 2017
2e74271
prepared for generator
tobkle Aug 18, 2017
b894088
after pull upstream/master and rebase upstream/master
tobkle Aug 18, 2017
84ad0cd
merged
tobkle Aug 18, 2017
d12860b
with generator
tobkle Aug 20, 2017
e14762b
remove yarn.lock error
tobkle Aug 25, 2017
5ab4acd
indexOf instead of includes polyfill problem
tobkle Aug 25, 2017
21d10b1
README.md file had changed
tobkle Aug 25, 2017
bf3dcae
without .DS_STORE
tobkle Aug 25, 2017
072171b
moved authorize.js to npm module
tobkle Aug 26, 2017
40fd007
modularize authorize
tobkle Aug 26, 2017
a69684f
with user password hash
tobkle Aug 27, 2017
4323c5d
generator implanted
tobkle Aug 31, 2017
9e2d358
with handlebars templating
tobkle Sep 2, 2017
073de89
with handlebars templating, without debugger
tobkle Sep 2, 2017
d5b7163
refactored: split authorization into packages, which are independent …
tobkle Sep 6, 2017
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,7 @@ dev.sqlite3
db
./output
dist
log/all-logs-readable.log
yarn-error.log
bin/test-model-generator.js

112 changes: 109 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,8 @@ type User {
}
```

You could update the generated `CreateUserInput` input object to take a `password` field:
The generator automatically adjusts your schema and code in the following way:
It adjusts the generated `CreateUserInput` input object to take a `password` field:

```graphql
input CreateUserInput {
Expand All @@ -125,7 +126,7 @@ input CreateUserInput {
}
```

And then update the generated `User` model to hash that password and store it:
And in updates the generated `User` model to hash that password and store it:

```js
import bcrypt from 'bcrypt';
Expand All @@ -151,7 +152,112 @@ class User {
}
```

### Client side code
So the "User" type is treated differently as any other default type.

## Authorization

create-graphql-server can also handle authorizations. There are two ways to control authorizations:
* Authorization by User-Roles
* Authorization by Document-Roles

**User-Roles** are stored in the User type. They group a number of users together, having all the same authorizations, defined with their role they play. Use it to define a user as a "super-administrator", "administrator", "publisher", "editor" or a "default-user". You are free to define your own names and numbers of User-Roles.

**Document-Roles** are stored in document fields which are referencing a User._id. They are there, to define fine grained authorizations for individual documents based on single user level. Use it to define an "owner", "creator", author" or "coauthor" of a document. Define the authorization they are allowed to do with this specific document. Here, the userId will be stored in the corresponding document field instead. If a user wants the specific authorization, it is tested, if this userId is stored in the document, if so, he gains access, if not, it is not allowed.

You can combine both models to define your authorization logic.

Add authorization logic by using a simple @authorize directive within your type definition. Use the following syntax:
```javascript
type ...
@authorize(
<role1>: [<list of authorizations>]
<role2>: [<list of authorizations>]
<role3>: [<list of authorizations>]
)
...
<fieldname1>: String @authRole("<role1>") // User-Role: Stores the Role name in the User type
<fieldname2>: User @authRole("<role2>") // Document-Role: Stores Single UserId
<fieldname3>: [User] @authRole("<role3>") // Document-Role: Stores Multiple UserIds
...
```

A \<role\> can be any name, you want to use for, to describe the authorizations of a group of users. A user can be assigned this \<role\> in a user type of any \<fieldname\>. You have to mark the relevant field name with the directive @authRole("\<role\>").

A list of authorizations can be:
* create: role is authorized to create a record of this type.
* read: role is authorized to readOne and readMany of this type.
* readOne: role is authorized to only readOne of this type.
* readMany: role is authorized to readMany of this type.
* update: role is authorized to update a record of this type.
* delete: role is authorized to delete a record of this type.

You can add any number of roles within a @authorize directive.

There is one pre-defined User-Role called "world". The world role includes all users, signed-in and not signed-in anonymous users. Use this role to define an authorization-list valid for all users.

#### Example on type User
```javascript
type User

@authorize(
admin: ["create", "read", "update", "delete"] // User-Role
this: ["read", "update", "delete"] // Document-Role
)

{
role: String @authRole("admin")
username: String!

bio: String
notify: Boolean

tweets(minLikes: Int): [Tweet!] @hasMany(as: "author")
liked: [Tweet!] @belongsToMany

following: [User!] @belongsToMany
followers: [User!] @hasAndBelongsToMany(as: "following")
}
```

#### Example on type Tweet

```javascript
type Tweet

@authorize(
admin: ["create", "read", "update", "delete"], // User-Role
world: ["read"] // User-Role
author: ["create", "read", "update", "delete"], // Document-Role
coauthors: ["read", "update"], // Document-Role
)

{
author: User! @unmodifiable @belongsTo @authRole("author")
coauthors: [User] @belongsTo @authRole("coauthors")
body: String!

likers: [User!] @hasAndBelongsToMany(as: "liked")
}
```

If you add these types with the create-graphql-server command-line-interface:
```bash
create-graphql-server add-type path/to/input.graphql
```

It will add automatically the authorization code in the model/\<type\>.js files. Have a look into the generated code or in the test application: "test/output-app/model/User.js".

You can manually change the code to protect also single fields of a type. Do it by the usage of this function:
```javascript
// userRole secretField document
docToInsert = protectFields(me, ['admin'], ['role'], docToInsert, { User: this.context.User });
```
By this, only a User (me) with role "admin" is allowed to access the field "docToInsert.role". For any other user, this field is removed from the docToInsert during the protectFields run.

## Example Implementation
[Please have a look at the example implementation.](https://github.com/tmeasday/create-graphql-server/tree/master/test/output-app)

## Client side code

To create users, simply call your generated `createUser` mutation (you may want to add authorization to the resolver, feel free to modify it).

Expand Down
1 change: 0 additions & 1 deletion bin/create-graphql-server.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,6 @@ function getFileUpdateList(inputSchemaFile, mode) {
// provide a dummy type file just with the type name
inputSchemaStr = `type ${adjustTypeName(inputSchemaFile)} {}`;
}

// generate code therefore in memory first
const {
typeName,
Expand Down
39 changes: 39 additions & 0 deletions bin/gentest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#!/usr/bin/env babel-node --inspect
/* eslint-disable no-console */
var exec = require('child_process').exec;
import fs from 'fs';
import path from 'path';
import os from 'os';
import minimist from 'minimist';
import generate from '../generate';

const argv = minimist(process.argv.slice(2));
const commands = argv._;
const file = commands[0] || path.join('test', 'input', 'User.graphql');
const targetDir = commands[1] || os.tmpdir();
const inputSchemaStr = fs.readFileSync(file, 'utf8');
const {
typeName,
TypeName,
outputSchemaStr,
resolversStr,
modelStr,
} = generate(inputSchemaStr);

console.log('\n\INPUT:\n\n', inputSchemaStr);
console.log('\n\nSCHEMA:\n\n', outputSchemaStr);
console.log('\n\MODEL:\n\n', modelStr);
console.log('\n\RESOLVER:\n\n', resolversStr, '\n\n');
writeFile(file, inputSchemaStr, '.input');
writeFile(file, outputSchemaStr, '.graphql');
writeFile(file, modelStr, '.model.js');
writeFile(file, resolversStr, '.resolver.js');

process.exit(0);

function writeFile(file, data, type) {
const newPath = path.join(targetDir, path.basename(file, '.graphql') + type);
console.log('writing to file and opening file in sublime editor...', newPath);
fs.writeFileSync(newPath, data, 'utf8');
exec(`subl ${newPath}`);
}
1 change: 0 additions & 1 deletion circle.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ dependencies:
- yarn:
pwd: test/output-app


test:
pre:
- npm start </dev/null &>/tmp/app-output:
Expand Down
15 changes: 6 additions & 9 deletions generate/index.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,23 @@
// We are in an intermediate step where we aren't actually generating files
// but we are generating code.

import { parse, print } from 'graphql';

import generateSchema from './schema';
import generateResolvers from './resolvers';
import generateModel from './model';
import { lcFirst } from './util/capitalization';
import generateModel from './model';
import generateResolvers from './resolvers';
import generateSchema from './schema';

export default function generate(inputSchemaStr) {
const inputSchema = parse(inputSchemaStr);

const type = inputSchema.definitions[0];
const TypeName = type.name.value;

const typeName = lcFirst(TypeName);
const outputSchema = generateSchema(inputSchema);
const outputSchemaStr = print(outputSchema);
const resolversStr = generateResolvers(inputSchema);
const modelStr = generateModel(inputSchema);

return {
typeName: lcFirst(TypeName),
typeName,
TypeName,
outputSchemaStr,
resolversStr,
Expand Down
112 changes: 15 additions & 97 deletions generate/model/index.js
Original file line number Diff line number Diff line change
@@ -1,107 +1,25 @@
import fs from 'fs';
import { print } from 'recast';
import { templateToAst } from '../util/read';
import getCode from '../util/getCode';
import { MODEL } from '../util/constants';
import { modulePath } from 'create-graphql-server-authorization';

import { templateToAst } from '../read';
import { lcFirst } from '../util/capitalization';
import generatePerField from '../util/generatePerField';

function read(name) {
return fs.readFileSync(`${__dirname}/templates/${name}.js.template`, 'utf8');
}

const templates = {
base: read('base'),
singularAssociation: read('singularAssociation'),
paginatedAssociation: read('paginatedAssociation'),
};

function buildAst(template, {
typeName,
fieldName,
argsStr,
ReturnTypeName,
query,
}) {
const argsWithDefaultsStr = argsStr
.replace('lastCreatedAt', 'lastCreatedAt = 0')
.replace('limit', 'limit = 10');
return templateToAst(template, {
typeName,
fieldName,
argsStr: argsWithDefaultsStr,
ReturnTypeName,
query,
});
export default function generateModel(inputSchema) {
const ast = generateModelAst(inputSchema);
return print(ast, { trailingComma: true }).code;
}


const generators = {
base({ typeName, TypeName }) {
return templateToAst(templates.base, { typeName, TypeName });
},
belongsTo(replacements) {
return buildAst(templates.singularAssociation, replacements);
},
belongsToMany(replacements) {
const { typeName, fieldName } = replacements;
return buildAst(templates.paginatedAssociation, {
...replacements,
query: `_id: { $in: ${typeName}.${fieldName}Ids || [] }`,
});
},
// TODO: write test and implement
// hasOne({ typeName, fieldName, ReturnTypeName }, { as }) {
// return templateToAst(templates.singularAssociation, {
// typeName,
// fieldName,
// ReturnTypeName,
// });
// },
hasMany(replacements, { as }) {
const { typeName } = replacements;
return buildAst(templates.paginatedAssociation, {
...replacements,
query: `${as || typeName}Id: ${typeName}._id`,
});
},
hasAndBelongsToMany(replacements, { as }) {
const { typeName } = replacements;
return buildAst(templates.paginatedAssociation, {
...replacements,
query: `${as || typeName}Ids: ${typeName}._id`,
});
},
};

export function generateModelAst(inputSchema) {
const type = inputSchema.definitions[0];
const TypeName = type.name.value;
const typeName = lcFirst(TypeName);

const ast = generators.base({ TypeName, typeName });

// XXX: rather than hardcoding in array indices it would be less brittle to
// walk the tree using https://github.com/benjamn/ast-types
const classMethodsAst = ast.program.body[2] // export
.declaration // class declaration
.body.body;

const findOneMethod = classMethodsAst.find(m => m.key.name === 'all');
let nextIndex = classMethodsAst.indexOf(findOneMethod) + 1;


generatePerField(type, generators).forEach((resolverFunctionAst) => {
const classMethodAst = resolverFunctionAst.program.body[0] // class declaration
.body.body[0]; // classMethod

classMethodsAst.splice(nextIndex, 0, classMethodAst);
nextIndex += 1;
const templateCode = getCode(MODEL, {
inputSchema,
basePath: [__dirname, 'templates'],
authPath: [modulePath, 'templates', 'model', 'auth']
});

return ast;
}
// validate syntax of generated template code
const replacements = {};
const ast = templateToAst(templateCode, replacements);

export default function generateModel(inputSchema) {
const ast = generateModelAst(inputSchema);
return print(ast, { trailingComma: true }).code;
return ast;
}
12 changes: 12 additions & 0 deletions generate/model/templates/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
In order to enable different authorization concepts,
the templates with a specific and opinionated authorization concept
are factored out, in a own npm module. You will find those templates
in the following npm module:

** node_modules/create-graphql-server-authorization/templates/auth **

Please feel free to fork create-graphql-server-authorization
and implement your own authorization concept there, or add your own
templates here.


Loading