Skip to content

Commit d5a4c32

Browse files
committed
add-user and authorization prototype
1 parent 9693275 commit d5a4c32

File tree

18 files changed

+1322
-114
lines changed

18 files changed

+1322
-114
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11

2+
0.5.5
3+
4+
- Added an authorization generator
5+
- Added a command add-user to generate user resolvers automatically
6+
27
0.5.4
38

49
- Added the possibility to run "add-type" several times, which overwrites existing types

README.md

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,152 @@
1+
*** WORK IN PROGRESS -- NOT YET FINISHED ***
2+
development is not yet finalized, and not yet tested
3+
This is to discuss the current approach to introduce a generator for authorizations also.
4+
5+
# Command **add-user**
6+
To add a new User type, use the following command:
7+
```bash
8+
create-graphql-server add-user path/to/user-input-type.graphql
9+
```
10+
11+
This generates a new user with the input type file. A new command was necessary, as the user type has a different resolver to handle password and hash.
12+
13+
If important fields with dependent logics are missing, they are added automatically by the generator, such as the fields: **email**, **password**, **role**.
14+
15+
# Authorization for types
16+
Use the new directive @authorize in the type definition to control the authorization settings:
17+
```javascript
18+
type User
19+
@authorize(
20+
create: ["owner"]
21+
read: ["world"]
22+
update: ["owner", "admin"]
23+
delete: ["owner", "admin"]
24+
ownerField: "id"
25+
roleField: "role"
26+
defaultUserRole: 'user',
27+
firstUserRole: 'admin',
28+
adminUserRole: 'admin',
29+
)
30+
{
31+
email: String!
32+
name: String
33+
role: String!
34+
}
35+
```
36+
or:
37+
```javascript
38+
type Post
39+
@authorize(
40+
create: ["owner"]
41+
read: ["world"]
42+
update: ["owner", "admin"]
43+
delete: ["owner", "admin"]
44+
ownerField: "ownerId"
45+
roleField: "role"
46+
)
47+
{
48+
post: String!
49+
comment: String
50+
owner: User1 @belongsTo
51+
}
52+
```
53+
54+
Meaning of the directive's arguments:
55+
* create = Authorization for a create mutation
56+
* read = Authorization for a read query
57+
* update = Authorization for a update mutation
58+
* delete = Authorization for a delete mutation
59+
*
60+
Add the authorized users in the array with...
61+
* "owner" = the user, who created the document
62+
* "world" = everyone is authorized
63+
* "admin" = the administrator, of the system
64+
* role = add any role, in the role field
65+
66+
The generator will create a new folder named authorization. In that folder, a new index.js file is copied into, which hosts the authorization logic. This authorization logic exposes only one central function **authorize()**, which will be called for each resolver's data handler. This authorize function should be only used in the resolver. It can send and receive whether an array or a document. It has the following signature:
67+
68+
```javascript
69+
const authorized_data = await authorize('TypeName', TypeName, 'mode', user, data);
70+
```
71+
whereas: 'TypeName' is a string with the current type name of the resolver, the passed TypeName contains the context of the resolver (Model,...), 'mode' contains a string with any of the available modes of the data access: 'create', 'read', 'update', 'delete', to inform the authorization module, what kind of data operation it shall authorize, user is the context object of the authenticated user, and input is the data which should be checked. The authorize function returns checked and authorized data. If for example one document of the data array is not accessible by the authenticated user, then it is filtered out and not included in the returned array/object.
72+
73+
Also, for each generated type, it creates an additional file in the authorization folder named by its type.js. In that file, all rules for a successfull authorization check is stored. You can adjust this file directly also afterwards as it keeps only data, with the following rules:
74+
75+
```javascript
76+
let defaultAuthorization = {
77+
name: ObjectTypeDefinition.name.value || 'nameNotFound',
78+
field: {
79+
ownerField: 'ownerId',
80+
roleField: 'role',
81+
},
82+
rules: [
83+
{
84+
mode: 'create',
85+
roles: ['owner'],
86+
removeFields: [],
87+
},
88+
{
89+
mode: 'read',
90+
roles: ['owner'],
91+
removeFields: [],
92+
},
93+
{
94+
mode: 'update',
95+
roles: ['owner', 'admin'],
96+
removeFields: [],
97+
},
98+
{
99+
mode: 'delete',
100+
roles: ['owner', 'admin'],
101+
removeFields: [],
102+
},
103+
]
104+
};
105+
```
106+
107+
```javascript
108+
let defaultUserAuthorization = {
109+
name: ObjectTypeDefinition.name.value || 'nameNotFound',
110+
isUser: true,
111+
defaultUserRole: 'user',
112+
firstUserRole: 'admin',
113+
adminUserRole: 'admin',
114+
field: {
115+
ownerField: 'id',
116+
roleField: 'role',
117+
},
118+
rules: [
119+
{
120+
mode: 'create',
121+
roles: ['world'],
122+
removeFields: ['role'],
123+
},
124+
{
125+
mode: 'read',
126+
roles: ['world'],
127+
removeFields: [],
128+
},
129+
{
130+
mode: 'update',
131+
roles: ['owner'],
132+
removeFields: ['role'],
133+
},
134+
{
135+
mode: 'update',
136+
roles: ['admin'],
137+
removeFields: [],
138+
},
139+
{
140+
mode: 'delete',
141+
roles: ['owner', 'admin'],
142+
removeFields: [],
143+
},
144+
]
145+
};
146+
```
147+
148+
If you don't enter any @authorize directive in your input file. It will generate the above rules for you automatically.
149+
1150
# Create GraphQL Server
2151

3152
This is a simple scaffolding tool for GraphQL apps, built on MongoDB.

bin/create-graphql-server.js

Lines changed: 63 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ function usage() {
3939
console.log(' - create-graphql-server init project-dir');
4040
console.log(' - create-graphql-server add-type path/to/type.graphql');
4141
console.log(' - create-graphql-server add-type path');
42+
console.log(' - create-graphql-server add-user path/to/UserType.graphql');
4243
console.log(' - create-graphql-server remove-type path/to/type.graphql');
4344
console.log(' - create-graphql-server remove-type path');
4445
console.log(' - create-graphql-server remove-type typename');
@@ -70,18 +71,21 @@ function usage() {
7071
process.exit(1);
7172
}
7273

73-
function adjustTypeName(typeName){
74+
function adjustTypeName(typeName) {
7475
return typeName.charAt(0).toUpperCase() + typeName.slice(1).toLowerCase();
7576
}
7677

7778
function getFileUpdateList(inputSchemaFile, mode) {
7879
let inputSchemaStr = '';
7980
// in add mode or if a graphql path/file name was provided, the input file must be there,
80-
if (mode === 'add' || inputSchemaFile.includes('.graphql') || inputSchemaFile.includes('/')) {
81-
if (! fs.existsSync(inputSchemaFile)) {
82-
console.error(
83-
chalk.bold.red('Error: Cannot read file', inputSchemaFile)
84-
);
81+
if (
82+
mode === 'add-type' ||
83+
mode === 'add-user' ||
84+
inputSchemaFile.includes('.graphql') ||
85+
inputSchemaFile.includes('/')
86+
) {
87+
if (!fs.existsSync(inputSchemaFile)) {
88+
console.error(chalk.bold.red('Error: Cannot read file', inputSchemaFile));
8589
console.log('');
8690
process.exit(1);
8791
}
@@ -100,7 +104,8 @@ function getFileUpdateList(inputSchemaFile, mode) {
100104
outputSchemaStr,
101105
resolversStr,
102106
modelStr,
103-
} = generate(inputSchemaStr);
107+
authorizationStr,
108+
} = generate(inputSchemaStr, mode);
104109

105110
// do validation checks
106111
// shouldn't be necessary, but...
@@ -115,7 +120,8 @@ function getFileUpdateList(inputSchemaFile, mode) {
115120
!modelStr ||
116121
outputSchemaStr === '' ||
117122
resolversStr === '' ||
118-
modelStr === ''
123+
modelStr === '' ||
124+
authorizationStr === ''
119125
) {
120126
console.error('Error: Error while generating target Code.');
121127
process.exit(0);
@@ -153,6 +159,13 @@ function getFileUpdateList(inputSchemaFile, mode) {
153159
indexPattern: `\nimport ${TypeName} from './${TypeName}';\n` +
154160
`models.${TypeName} = ${TypeName};\n`,
155161
},
162+
{
163+
typePath: path.join('authorization', `${TypeName}.js`),
164+
typeString: authorizationStr,
165+
indexPath: path.join('authorization', 'index.js'),
166+
indexPattern: `\nimport ${TypeName} from './${TypeName}';\n` +
167+
`authorizations.${TypeName} = ${TypeName};\n`,
168+
},
156169
];
157170
}
158171

@@ -390,16 +403,19 @@ function addPatternToFiles(fileUpdateList) {
390403

391404
function getFilesRecursively(folder, filetype) {
392405
// getting all files of a path and of a specific filetype recursively
393-
let list = [], stats,
394-
files = fs.readdirSync(folder);
406+
let list = [];
407+
let stats;
408+
const files = fs.readdirSync(folder);
395409

396-
files.forEach(file => {
410+
files.forEach((file) => {
397411
stats = fs.lstatSync(path.join(folder, file));
398-
if(stats.isDirectory()) {
399-
list = list.concat(getFilesRecursively(path.join(folder, file), filetype));
412+
if (stats.isDirectory()) {
413+
list = list.concat(
414+
getFilesRecursively(path.join(folder, file), filetype)
415+
);
400416
} else if (file.includes(filetype)) {
401-
console.log('found:', path.join(folder, file));
402-
list.push(path.join(folder, file));
417+
console.log('found:', path.join(folder, file));
418+
list.push(path.join(folder, file));
403419
}
404420
});
405421

@@ -408,10 +424,10 @@ function getFilesRecursively(folder, filetype) {
408424

409425
// MAIN FUNCTIONS
410426

411-
function addType(inputSchemaFile, options) {
427+
function addType(inputSchemaFile, options, mode) {
412428
// generates a new data type with all its files and <type> references
413429
console.log(chalk.bold.blue('Running add-type'));
414-
const fileUpdateList = getFileUpdateList(inputSchemaFile, 'add');
430+
const fileUpdateList = getFileUpdateList(inputSchemaFile, mode);
415431
checkForFileChanges(fileUpdateList, options['force-update']);
416432
createTypeFiles(fileUpdateList);
417433
addPatternToFiles(fileUpdateList);
@@ -455,33 +471,56 @@ if (commands[0] === 'init') {
455471
if (!inputSchemaFile) {
456472
usage();
457473
}
458-
if (fs.existsSync(inputSchemaFile) && fs.lstatSync(inputSchemaFile).isDirectory()) {
459-
//directory name entered
474+
if (
475+
fs.existsSync(inputSchemaFile) &&
476+
fs.lstatSync(inputSchemaFile).isDirectory()
477+
) {
478+
// directory name entered
460479
const files = getFilesRecursively(inputSchemaFile, '.graphql');
461-
files.forEach(file => {
462-
addType(file, argv);
480+
files.forEach((file) => {
481+
addType(file, argv, commands[0]);
463482
});
464483
} else {
465484
// single file entered
466-
addType(inputSchemaFile, argv);
485+
addType(inputSchemaFile, argv, commands[0]);
467486
}
468487
process.exit(0);
469488
} else if (commands[0] === 'remove-type') {
470489
const inputSchemaFile = commands[1];
471490
if (!inputSchemaFile) {
472491
usage();
473492
}
474-
if (fs.existsSync(inputSchemaFile) && fs.lstatSync(inputSchemaFile).isDirectory()) {
493+
if (
494+
fs.existsSync(inputSchemaFile) &&
495+
fs.lstatSync(inputSchemaFile).isDirectory()
496+
) {
475497
// directory name entered
476498
const files = getFilesRecursively(inputSchemaFile, '.graphql');
477-
files.forEach(file => {
499+
files.forEach((file) => {
478500
removeType(file, argv);
479501
});
480502
} else {
481503
// single file or type
482504
removeType(inputSchemaFile, argv);
483505
}
484506
process.exit(0);
507+
} else if (commands[0] === 'add-user') {
508+
const inputSchemaFile = commands[1];
509+
if (!inputSchemaFile) {
510+
usage();
511+
}
512+
if (
513+
fs.existsSync(inputSchemaFile) &&
514+
fs.lstatSync(inputSchemaFile).isDirectory()
515+
) {
516+
// directory name entered
517+
console.error('Please enter a User Type instead of a directory.');
518+
process.exit(0);
519+
} else {
520+
// single file entered
521+
addType(inputSchemaFile, argv, commands[0]);
522+
}
523+
process.exit(0);
485524
} else {
486525
usage();
487526
}

0 commit comments

Comments
 (0)