Forked from nateps/gist:02d0b0293880905476bd
Last active
August 29, 2015 14:07
Revisions
-
nateps created this gist
Jul 12, 2014 .There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,222 @@ # Whitelist collections ALLOW_COLLECTIONS = { 'accounts': true 'users': true } module.exports = (shareClient) -> # Hold on to session object for later use. The HTTP req object is only # available in the connect event shareClient.use 'connect', (shareRequest, next) -> shareRequest.agent.connectSession = shareRequest.req.session next() shareClient.use (shareRequest, next) -> unless ALLOW_COLLECTIONS[shareRequest.collection] return next '403: Cannot access collection' next() shareClient.use 'query', (shareRequest, next) -> validateQueryRead( shareRequest.agent, shareRequest.collection, shareRequest.query, next ) shareClient.filter (collection, docName, docData, next) -> validateDocRead( this, collection, docName, docData.data, next ) shareClient.use 'submit', (shareRequest, next) -> opData = shareRequest.opData opData.connectSession = shareRequest.agent.connectSession opData.collection = shareRequest.collection opData.docName = shareRequest.docName next() shareClient.preValidate = (opData, docData) -> # Validators is a list of functions to be called in the validate hook with # the mutated document data. Note that ShareJS mutates the document without # copying it first, so any values being compared against the original # document should be cached by value in the closure scope. They should NOT # be accessed from the document object in the validator opData.validators = [] # A ShareJS op is a list of mutations to be applied to a given document. # Most of the time, this will be a single mutation. If so, we only have # to check against that particular path if !opData.op || opData.op.length is 1 return preValidateWrite( opData, opData.collection, opData.docName, opData.op?[0].p || [], docData.data ) # Otherwise, we need to check for an error for each unique path being # modified within the document pathMap = {} for component in opData.op path = component.p || [] key = path.join '.' pathMap[key] = component.p for key, path of pathMap err = preValidateWrite( opData, opData.collection, opData.docName, path, docData.data ) return err if err return shareClient.validate = (opData, docData) -> return unless opData.validators.length doc = docData.data for fn in opData.validators err = fn doc, opData return err if err return # Validating all possible Mongo queries is really difficult and not recommended. # This is a simple validator that will at least restrict queries to those that # contain an accountId, which provides a good first level of protection. The # docs returned by the query are also validated, so while it is possible to leak # data via well targeted queries, specifically restricting every query type is # not recommended. Better than all of this is to not allow any queries created # in the browser, and only create queries on the server. This could be done # in a ShareJS middleware. validateQueryRead = (agent, collection, query, next) -> session = agent.connectSession userId = session?.userId accountId = session?.accountId unless query return next '403: No query specified' unless session console.error 'Warning: Query read access no session ', collection, query return next '403: No session' unless userId console.error 'Warning: Query read access no session.userId ', collection, query, session return next '403: No session.userId' unless accountId console.error 'Warning: Query read access no session.accountId ', collection, query, session return next '403: No session.accountId' query = query.$query if query.$query # Protect queries by account. This gives us a simple base level of security # from the most dangerous threat of outsiders gaining access. For more # complex access control within accounts, we rely on access control of # specific documents below. Note that this does not gaurd against queries # to find out if specific documents exist or not within an account. A more # ideal solution would not indicate when a document exists that the user # does not have access to. if collection is 'accounts' return next() if query._id is accountId return next '403: Cannot query accounts that are not yours.' return next() if query.accountId is accountId return next "403: Cannot query #{collection} from a different account." validateDocRead = (agent, collection, docId, doc, next) -> session = agent.connectSession userId = session?.userId accountId = session?.accountId unless session console.error 'Warning: Doc read access no session ', collection, docId return next '403: No session' unless userId console.error 'Warning: Doc read access no session.userId ', collection, docId, session return next '403: No session.userId' unless accountId console.error 'Warning: Doc read access no session.accountId ', collection, docId, session return next '403: No session.accountId' # Don't allow any user to access a document in a different account unless docMatchesAccountId collection, docId, doc, accountId return next "403: Cannot access document from another account #{collection}.#{docId}" ## APP SPECIFIC ACCESS RULES HERE ## # Allow access to all documents within an account return next() # This function must be synchronous for important performance reasons. Any data # needed to check access control rules must be fetched and stored on the session # becuase the write is submitted preValidateWrite = (opData, collection, docId, path, doc) -> session = opData.connectSession userId = session?.userId accountId = session?.accountId unless session console.error 'Warning: Write access no session', arguments... return '403: No session' unless userId console.error 'Warning: Write access no session.userId', arguments... return '403: No session.userId' unless accountId console.error 'Warning: Write access no session.accountId', arguments... return '403: No session.accountId' doc ||= opData.create?.data validators = opData.validators # Don't allow any user to modify a document in a different account unless doc console.error 'Error: No document snapshot or create data', arguments... return '403: No document snapshot or create data' unless docMatchesAccountId collection, docId, doc, accountId return "403: Account cannot modify document #{collection}.#{docId}" # As a general pattern, if a user can typically edit a document type except # under certain conditions, a validator function must be used to ensure that # the condition is met after mutation. In such blacklisting cases, checking # the path is NOT sufficient, as the entire document or a parent path might # be edited instead. # # In contrast, if a user cannot typically edit a document type. It is OK # to whitelist specific modifications by path. if collection is 'accounts' return '403: Cannot modify accounts' else # Ensure documents have a matching accountId after mutation validators.push (mutatedDoc) -> return if !mutatedDoc || mutatedDoc.accountId == accountId return '403: Cannot modify a document to have a different accountId' if collection is 'users' # A user can modify whitelisted fields on their own document if docId is userId return if path[0] in ['name', 'email'] return "403: Cannot modify #{path} of #{collection}.#{docId}" # Users can't modify other users return '403: Cannot modify user who is not you' ## APP SPECIFIC ACCESS RULES HERE ## # Allow all other changes return docMatchesAccountId = (collection, docId, data, accountId) -> return false unless collection && docId && data && accountId return docId is accountId if collection is 'accounts' return data.accountId is accountId # Note that additional documents required to do read access control can be # fetched from the ShareJS agent directly. It is much better to use ShareJS # doc fetches instead of adding the overhead and potential for memory leaks # from Racer models. Example: checkSecret = (collection, docId, userId, cb) -> unless docId return cb '403: Cannot access document missing id reference' agent.fetch collection, docId, (err, doc) -> return cb err if err return cb() unless doc.data?.secretTo == userId cb '403: Cannot access secret document'