Skip to content

Instantly share code, notes, and snippets.

@brysgo
Created May 17, 2018 16:59
Show Gist options
  • Save brysgo/494f7dfa2e88fa23bfe0aa90274149d0 to your computer and use it in GitHub Desktop.
Save brysgo/494f7dfa2e88fa23bfe0aa90274149d0 to your computer and use it in GitHub Desktop.
Add support for promises and fix sub-object bug
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