Skip to content

Instantly share code, notes, and snippets.

@nateps
Created July 12, 2014 00:45
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save nateps/02d0b0293880905476bd to your computer and use it in GitHub Desktop.
Save nateps/02d0b0293880905476bd to your computer and use it in GitHub Desktop.
Example ShareJS access control middleware for use with Derby
# 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'
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment