Instantly share code, notes, and snippets.
Created
May 17, 2018 16:59
-
Star
0
(0)
You must be signed in to star a gist -
Fork
0
(0)
You must be signed in to fork a gist
-
Save brysgo/494f7dfa2e88fa23bfe0aa90274149d0 to your computer and use it in GitHub Desktop.
Add support for promises and fix sub-object bug
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const _ = require("lodash"); | |
function resolveAuthLevel(schema, options, doc) { | |
// Look into options the options and try to find authLevels. Always prefer to take | |
// authLevels from the direct authLevel option as opposed to the computed | |
// ones from getAuthLevel in the schema object. | |
let authLevelsIn = []; | |
if (options) { | |
if (options.authLevel) { | |
authLevelsIn = options.authLevel; | |
} else if (typeof schema.getAuthLevel === "function") { | |
authLevelsIn = schema.getAuthLevel(options.authPayload, doc); | |
} | |
} | |
return Promise.resolve(authLevelsIn).then(function(authLevels) { | |
authLevels = _.castArray(authLevels); | |
// Add `defaults` to the list of levels since you should always be able to do what's specified | |
// in defaults. | |
authLevels.push("defaults"); | |
const perms = schema.permissions || {}; | |
return _.chain(authLevels) | |
.filter(level => !!perms[level]) // make sure the level in the permissions dict | |
.uniq() // get rid of fields mentioned in multiple levels | |
.value(); | |
}); | |
} | |
function getAuthorizedFields(schema, options, action, doc) { | |
return resolveAuthLevel(schema, options, doc).then(function(authLevels) { | |
return _.chain(authLevels) | |
.map(level => schema.permissions[level][action]) | |
.flatten() | |
.uniq() // dropping duplicates | |
.value(); | |
}); | |
} | |
function hasPermission(schema, options, action, doc) { | |
return resolveAuthLevel(schema, options, doc).then(function(authLevels) { | |
const perms = schema.permissions || {}; | |
// look for any permissions setting for this action that is set to true (for these authLevels) | |
return _.some(authLevels, level => perms[level][action]); | |
}); | |
} | |
const PermissionDeniedError = require("mongoose-authorization/lib/PermissionDeniedError"); | |
// TODO implement a pluginOption for putting the permissions into the results object for | |
// find queries | |
module.exports = schema => { | |
let authorizationEnabled = true; | |
function save(doc, options, next) { | |
hasPermission(schema, options, "create", doc) | |
.then(function(permitted) { | |
if (doc.isNew && !permitted) { | |
return next(new PermissionDeniedError("create")); | |
} | |
}) | |
.then(function() { | |
return getAuthorizedFields(schema, options, "write", doc).then(function( | |
authorizedFields | |
) { | |
const modifiedPaths = doc.modifiedPaths(); | |
const discrepancies = _.difference(modifiedPaths, authorizedFields); | |
if (discrepancies.length > 0) { | |
return next(new PermissionDeniedError("write", discrepancies)); | |
} | |
next(); | |
}); | |
}); | |
} | |
function removeQuery(query, next) { | |
hasPermission(schema, query.options, "remove").then(function(permitted) { | |
if (permitted) { | |
next(); | |
} else { | |
next(new PermissionDeniedError("remove")); | |
} | |
}); | |
} | |
function removeDoc(doc, options, next) { | |
hasPermission(schema, query.options, "remove").then(function(permitted) { | |
if (permitted) { | |
next(); | |
} else { | |
next(new PermissionDeniedError("remove")); | |
} | |
}); | |
} | |
function find(query, docs, next) { | |
const docList = _.castArray(docs); | |
const multi = docList.length; | |
const processedResultPromises = _.map(docList, doc => { | |
if (!doc) { | |
return Promise.resolve(doc); | |
} | |
return getAuthorizedFields(schema, query.options, "read", doc).then( | |
function(authorizedFields) { | |
if (getAuthorizedFields.length === 0) { | |
return; | |
} | |
// Check to see if group has the permission to see the fields that came back. Fields | |
// that don't will be removed. | |
const authorizedFieldsSet = new Set(authorizedFields); | |
const innerDoc = doc._doc || doc; | |
for (const pathName of _.keys(innerDoc)) { | |
if (!authorizedFieldsSet.has(pathName)) { | |
delete innerDoc[pathName]; | |
} | |
} | |
// Special work. Wipe out the getter for the virtuals that have been set on the | |
// schema that are not authorized to come back | |
for (const pathName of _.keys(schema.virtuals)) { | |
if (!authorizedFieldsSet.has(pathName)) { | |
// These virtuals are set with `Object.defineProperty`. You cannot overwrite them | |
// by directly setting the value to undefined, or by deleting the key in the | |
// document. This is potentially slow with lots of virtuals | |
Object.defineProperty(doc, pathName, { | |
value: undefined | |
}); | |
} | |
} | |
return _.isEmpty(innerDoc) ? undefined : doc; | |
} | |
); | |
}); | |
Promise.all(processedResultPromises).then(function(processedResult) { | |
const filteredResult = _.filter(processedResult); | |
next(null, multi ? filteredResult : filteredResult[0]); | |
}); | |
} | |
function update(query, next) { | |
hasPermission(schema, query.options, "create") | |
.then(function(permitted) { | |
// If this is an upsert, you'll need the create permission | |
if (query.options && query.options.upsert && !permitted) { | |
return next(new PermissionDeniedError("create")); | |
} | |
}) | |
.then(function() { | |
return getAuthorizedFields(schema, query.options, "write").then( | |
function(authorizedFields) { | |
// create an update object that has been sanitized based on permissions | |
const sanitizedUpdate = {}; | |
authorizedFields.forEach(field => { | |
sanitizedUpdate[field] = query._update[field]; | |
}); | |
// check to see if the group is trying to update a field it does not have permission to | |
const discrepancies = _.difference( | |
Object.keys(query._update), | |
Object.keys(sanitizedUpdate) | |
); | |
if (discrepancies.length > 0) { | |
next(new PermissionDeniedError("write", discrepancies)); | |
return; | |
} | |
query._update = sanitizedUpdate; | |
// TODO, see if this section works at all. Seems off that the `_fields` property is the | |
// thing that determines what fields come back | |
// Detect which fields can be returned if 'new: true' is set | |
const authorizedReturnFields = getAuthorizedFields( | |
schema, | |
query.options, | |
"read" | |
); | |
// create a sanitizedReturnFields object that will be used to return only the fields that a | |
// group has access to read | |
const sanitizedReturnFields = {}; | |
for (const field of authorizedReturnFields) { | |
if (!query._fields || query._fields[field]) { | |
sanitizedReturnFields[field] = 1; | |
} | |
} | |
query._fields = sanitizedReturnFields; | |
next(); | |
} | |
); | |
}); | |
} | |
schema.pre("findOneAndRemove", function preFindOneAndRemove(next) { | |
if (!authorizationEnabled) { | |
next(); | |
return; | |
} | |
removeQuery(this, next); | |
}); | |
// TODO, WTF, how to prevent someone from Model.find().remove().exec(); That doesn't | |
// fire any remove hooks. Does it fire a find hook? | |
schema.pre("remove", function preRemove(next, options) { | |
if (!authorizationEnabled) { | |
next(); | |
return; | |
} | |
removeDoc(this, options, next); | |
}); | |
schema.pre("save", function preSave(next, options) { | |
if (!authorizationEnabled) { | |
next(); | |
return; | |
} | |
save(this, options, next); | |
}); | |
schema.post("find", function postFind(doc, next) { | |
if (!authorizationEnabled) { | |
next(); | |
return; | |
} | |
find(this, doc, next); | |
}); | |
schema.post("findOne", function postFindOne(doc, next) { | |
if (!authorizationEnabled) { | |
next(); | |
return; | |
} | |
find(this, doc, next); | |
}); | |
schema.pre("update", function preUpdate(next) { | |
if (!authorizationEnabled) { | |
next(); | |
return; | |
} | |
update(this, next); | |
}); | |
schema.pre("findOneAndUpdate", function preFindOneAndUpdate(next) { | |
if (!authorizationEnabled) { | |
next(); | |
return; | |
} | |
update(this, next); | |
}); | |
schema.query.setAuthLevel = function setAuthLevel(authLevel) { | |
this.options.authLevel = authLevel; | |
return this; | |
}; | |
schema.static("disableAuthorization", () => { | |
authorizationEnabled = false; | |
}); | |
schema.static("enableAuthorization", () => { | |
authorizationEnabled = true; | |
}); | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment