Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: 352Media/mongoose-authorization
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: master
Choose a base ref
...
head repository: devcolor/mongoose-authz
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: master
Choose a head ref
Able to merge. These branches can be automatically merged.
Loading
Showing with 8,832 additions and 92 deletions.
  1. +7 −0 .eslintrc.json
  2. +2 −1 .gitignore
  3. +7 −0 .jscsrc
  4. +4 −0 .npmignore
  5. +10 −0 .travis.yml
  6. +10 −0 .vscode/settings.json
  7. +7 −0 CHANGELOG.md
  8. +207 −1 README.md
  9. +19 −0 __tests__/authIsDisabled.test.js
  10. +78 −0 __tests__/cleanAuthLevels.test.js
  11. +175 −0 __tests__/embedPermissions.test.js
  12. +14 −0 __tests__/exampleSchemas/bareBonesSchema.js
  13. +47 −0 __tests__/exampleSchemas/goodSchema.js
  14. +39 −0 __tests__/getAuthorizedActions.test.js
  15. +75 −0 __tests__/getAuthorizedFields.test.js
  16. +3 −0 __tests__/getEmbeddedPermission.test.js
  17. +3 −0 __tests__/getUpdatePaths.test.js
  18. +3 −0 __tests__/hasPermission.test.js
  19. +3 −0 __tests__/methods/Document.save.test.js
  20. +3 −0 __tests__/methods/Document.update.test.js
  21. +3 −0 __tests__/methods/Model.aggreate.test.js
  22. +3 −0 __tests__/methods/Model.bulkWrite.test.js
  23. +3 −0 __tests__/methods/Model.count.test.js
  24. +36 −0 __tests__/methods/Model.create.test.js
  25. +3 −0 __tests__/methods/Model.deleteMany.test.js
  26. +3 −0 __tests__/methods/Model.deleteOne.test.js
  27. +3 −0 __tests__/methods/Model.distinct.test.js
  28. +3 −0 __tests__/methods/Model.find.test.js
  29. +3 −0 __tests__/methods/Model.findById.test.js
  30. +3 −0 __tests__/methods/Model.findByIdAndDelete.test.js
  31. +3 −0 __tests__/methods/Model.findByIdAndRemove.test.js
  32. +3 −0 __tests__/methods/Model.findByIdAndUpdate.test.js
  33. +3 −0 __tests__/methods/Model.findOne.test.js
  34. +3 −0 __tests__/methods/Model.findOneAndDelete.test.js
  35. +3 −0 __tests__/methods/Model.findOneAndRemove.test.js
  36. +3 −0 __tests__/methods/Model.findOneAndUpdate.test.js
  37. +3 −0 __tests__/methods/Model.geoSearch.test.js
  38. +3 −0 __tests__/methods/Model.increment.test.js
  39. +3 −0 __tests__/methods/Model.isertMany.test.js
  40. +3 −0 __tests__/methods/Model.mapReduce.test.js
  41. +36 −0 __tests__/methods/Model.remove.test.js
  42. +3 −0 __tests__/methods/Model.replaceOne.test.js
  43. +3 −0 __tests__/methods/Model.update.test.js
  44. +3 −0 __tests__/methods/Model.updateMany.test.js
  45. +3 −0 __tests__/methods/Model.updateOne.test.js
  46. +3 −0 __tests__/resolveAuthLevel.test.js
  47. +4 −0 __tests__/sanitizeDocumentList.test.js
  48. +191 −82 index.js
  49. +6,510 −0 package-lock.json
  50. +40 −8 package.json
  51. +9 −0 src/IncompatibleMethodError.js
  52. +11 −0 src/PermissionDeniedError.js
  53. +5 −0 src/authIsDisabled.js
  54. +11 −0 src/cleanAuthLevels.js
  55. +45 −0 src/embedPermissions.js
  56. +14 −0 src/getAuthorizedActions.js
  57. +15 −0 src/getAuthorizedFields.js
  58. +10 −0 src/getEmbeddedPermission.js
  59. +16 −0 src/getUpdatePaths.js
  60. +12 −0 src/hasPermission.js
  61. +30 −0 src/resolveAuthLevel.js
  62. +82 −0 src/sanitizeDocumentList.js
  63. +57 −0 test/car.schema.js
  64. +203 −0 test/helpers.test.js
  65. +596 −0 test/index.test.js
  66. +24 −0 test/location.schema.js
  67. +87 −0 test/user.schema.js
7 changes: 7 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"extends": "airbnb-base",
"rules": {
"no-underscore-dangle": ["error", { "allow": ["_conditions", "_fields", "_update", "_doc", "_id"] }],
"no-param-reassign": ["error", { "props": false }]
}
}
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
.idea/
.idea/
node_modules/
7 changes: 7 additions & 0 deletions .jscsrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"preset": "google",
"fileExtensions": [".js", "jscs"],
"excludeFiles": [],
"maximumLineLength": 1000,
"requireCamelCaseOrUpperCaseIdentifiers": "ignoreProperties"
}
4 changes: 4 additions & 0 deletions .npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
test/
README.md
.gitignore
.travis.yml
10 changes: 10 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
language: node_js
services:
- mongodb
node_js:
- "8.3.0"
before_script:
- sleep 15
script:
- eslint .
- npm test
10 changes: 10 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"cSpell.words": [
"adhoc",
"authz",
"nodeunit",
"permissioned",
"subpath",
"upsert"
]
}
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# 2.0.0

- Removing the ability to call Model.remove() and Model.create() since those aren't compatible with how this library works.
- Muuuuch better tests
- Embedded permissions object cannot be overwritten
- When a document has embedded permissions, those permissions will be checks when a save or remove is being done. That way someone cannot write to an object in a way that changes their permissions and then try to save it.
- Does not add permissions when a doc is empty
208 changes: 207 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,207 @@
# mongoose-authorization
# mongoose-authz
[![Build Status](https://travis-ci.org/devcolor/mongoose-authz.svg)](https://travis-ci.org/devcolor/mongoose-authz)

This plugin allows you to define a custom authorization scheme on your mongoose models.

`npm install --save mongoose-authz`


## Getting Started

```javascript
'use strict';
var mongoose = require('mongoose');
var authz = require('mongoose-authz');

var userSchema = new mongoose.Schema({
email: {
type: String,
required: true,
unique: true
},
first_name: {
type: String,
required: true
},
last_name: {
type: String,
required: true
},
avatar: {
type: String
},
last_login_date: {
type: Date
},
status: {
type: String,
required: true,
default: 'active'
}
});

/*
* Make sure you add this before compiling your model
*/
userSchema.permissions = {
defaults: {
read: ['email', 'first_name', 'last_name', 'avatar']
},
admin: {
read: ['status'],
write: ['status'],
create: true,
actions: ['merge'],
},
owner: {
read: ['status'],
write: ['email', 'first_name', 'last_name', 'avatar'],
remove: true
}
};

userSchema.plugin(authz);

module.exports = mongoose.model('users', userSchema);
```

In the example above we extended the **userSchema** by adding a *permissions* object. This will not persist to your documents.

The permissions object consists of properties that represent your authorization levels (or groups). For each group, there are 4 permissions you can configure.
* `create` - Boolean
* `remove` - Boolean
* `write` - [array of fields] *NOTE: if `upsert: true`, the group will need to have `create` permissions too*
* `read` - [array of fields]
* `actions` - An array or arbitrary strings that lets you define more complicated actions (usually involving multiple fields or documents) that are allowed. Your own application code will need to handle the updates (likely disabling field level checking for those updates).

You can also specify a `defaults` group, which represents permissions that are available to all groups.

If you need the document in order to determine the correct authorization level for an action, you can place a static `getAuthLevel` function directly in your schema. For applicable actions, this function will be called with a specific document and a payload of data specified in the query. This is useful when the authorization level depends on matching properties of a user with properties of a specific document to determine if *that* user can modify *that* document.

###### *NOTE: The `getAuthLevel` approach does not work for update or remove queries since the document is not loaded into memory.*

```javascript
var mongoose = require('mongoose');

var carSchema = new mongoose.Schema({
make: {
type: String,
required: true,
unique: true
},
model: {
type: String,
required: true
},
year: {
type: Number
},
plate: {
type: String
}
});

/*
* Make sure you add this before compiling your model
*/
carSchema.permissions = {
defaults: {
read: ['_id', 'make', 'model', 'year']
},
maker: {
write: ['make', 'model', 'year'],
remove: true
},
dealer: {
read: ['plate'],
write: ['plate']
}
};

carSchema.getAuthLevel = function (payload, doc) {
if (payload && doc && payload.companyName === doc.make) {
return 'maker';
}

return 'dealer';
}
```

In you application code, you could then do the following:

```javascript
Car.find({}, null, { authPayload: { companyName: 'Toyota' } }).exec(...);

// or

myCar.save({ authPayload: { companyName: 'Honda' } });
```

You can also have the permissions for a specific document injected into the document when returned from a find query using the `permissions` option on the query. The permissions will be inserted into the object using the key `permissions` unless you specify the desired key name as the permissions option.

```javascript
const user = await User.find().setOptions({ authLevel: 'admin': permissions: true }).exec();

console.log(user.permissions);
// Outputs:
// {
// read: [...],
// write: [...],
// remove: [boolean],
// actions: [...],
// }

// OR
const user = await User.find().setOptions({ authLevel: 'admin': permissions: 'foo' }).exec();

console.log(user.foo);
// Outputs:
// {
// read: [...],
// write: [...],
// remove: [boolean],
// actions: [...],
// }
```

#### Example Uses

###### NOTE: If no authLevel is able to be determined, permission to perform the action will be denied. If you would like to circumvent authorization, pass `false` as the authLevel (e.g. `myModel.find().setAuhtLevel(false).exec();`, which will disable authorization for that specific query).

***example update***

You can also specify an array of authentication levels. This would merge the settings of each auth level.

```javascript
users.update({user_id: userUpdate.user_id}, userUpdate, {
authLevel: 'admin'
}, function(err, doc) {
if (err) {
//handle error
} else {
//success
}
});
```


###### *NOTE: When using `findOneAndUpdate`, the return document will be sanitized based on the group's permissions for `read`*

```javascript
await users.findOneAndUpdate(
{ user_id: userUpdate.user_id },
userUpdate,
{ authLevel: 'admin'}
);
```


***example find***

```javascript
await users.find(
{ user_id: userUpdate.user_id },
null,
{ authLevel: 'admin' }
);
```
19 changes: 19 additions & 0 deletions __tests__/authIsDisabled.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
const test = require('ava');
const authIsDisabled = require('../src/authIsDisabled');

test.todo('Create tests for authIsDisabled');
test('works with no options', (t) => {
t.false(authIsDisabled(), 'should handle undefined input');
t.false(authIsDisabled(false), 'should handle false input');
t.false(authIsDisabled(''), 'should handle empty string input');
t.false(authIsDisabled({}), 'should handle empty object input');
});

test('handles authLevel correctly', (t) => {
t.true(authIsDisabled({ authLevel: false }), 'false AuthLevel should disable authorization');
t.false(authIsDisabled({ authLevel: 0 }), 'authLevel of 0 should not disable authorization');
t.false(
authIsDisabled({ authLevel: '' }),
'authLevel of empty string should not disable authorization',
);
});
78 changes: 78 additions & 0 deletions __tests__/cleanAuthLevels.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
const test = require('ava');
const mongoose = require('mongoose');
const cleanAuthLevels = require('../src/cleanAuthLevels');

test.before((t) => {
const schema = new mongoose.Schema({ friend: String });
schema.permissions = { default: {}, manager: {}, report: {} };
t.context.schema = schema;
});

test('Schema passed in is not valid', (t) => {
t.deepEqual(
cleanAuthLevels(false, ['foo']),
[],
'should return empty list when schema is falsy',
);

t.deepEqual(
cleanAuthLevels(false, []),
[],
'should return empty list when schema is falsy and there are no authLevels',
);

t.deepEqual(
cleanAuthLevels({}, ['foo']),
[],
'should return empty list when schema is empty object',
);

t.deepEqual(
cleanAuthLevels({}, []),
[],
'should return empty list when schema is empty object & there are no authLevels',
);
});

test('Falsey authLevel value', (t) => {
t.deepEqual(cleanAuthLevels(t.context.schema, false), []);
t.deepEqual(cleanAuthLevels(t.context.schema, 0), []);
});

test('Empty array authLevel value', (t) => {
t.deepEqual(cleanAuthLevels(t.context.schema, []), []);
});

test('Remove duplicate entries', (t) => {
t.deepEqual(cleanAuthLevels(t.context.schema, ['manager', 'manager']), ['manager']);
});

test('Remove false entries', (t) => {
t.deepEqual(cleanAuthLevels(t.context.schema, [0, 'manager', false]), ['manager']);
});

test('Remove entries that are not in the permissions object', (t) => {
t.deepEqual(cleanAuthLevels(t.context.schema, ['fake', 'manager', 'notthere']), ['manager']);
t.deepEqual(cleanAuthLevels(t.context.schema, ['fake', 'notthere']), []);
});

test('authLevel with no issues', (t) => {
t.deepEqual(
cleanAuthLevels(t.context.schema, ['manager']),
['manager'],
'should handle single item array',
);

t.deepEqual(
cleanAuthLevels(t.context.schema, ['manager', 'report']),
['manager', 'report'],
'should handle mutliple item array',
);

t.deepEqual(
cleanAuthLevels(t.context.schema, 'manager'),
['manager'],
'should handle single level, not in array',
);
});

Loading