Skip to content

Instantly share code, notes, and snippets.

@ile
Forked from nateps/gist:02d0b0293880905476bd
Last active August 29, 2015 14:07

Revisions

  1. @nateps nateps created this gist Jul 12, 2014.
    222 changes: 222 additions & 0 deletions gistfile1.coffee
    Original 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'