Security Extensions in SonarW

Localhost Exception Extension

SonarW has a built-in role called localhost_only. Assigning this role to a user means that this user may only login to SonarW from the localhost.

Preventing a Drop Collection

Prevent a collection from bring dropped using the undroppable command:

db.runCommand({undroppable:{acollection:true}})

If someone then tries to drop the collection acollection it will result in an error. Use this to prevent accidental drops or use it as a security machanism when you want the database to be used as a secure archive where data can never be truly deleted or changed (remember that a remove or an update do not delete/change the old data, they merely create a new version of the data).

Restore normal behavior by using the runCommand with an argument of false:

db.runCommand({undroppable:{acollection:false}})

Query the current state of the collection using:

db.runCommand({undroppable:"acollection"})

This will return true or false depeneding on whether the collection is undroppable.

Auditing Extensions in SonarW

SonarW has an auditing subsystem that allows you to maintain an audit trail of who did what in the system.

By default auditing is disabled in SonarW. To enable auditing you need to shutdown your server and restart it with the audit_on=true configuration entry.

Once auditing has been enabled, audit records are created based on a set of rules. Audit rules are created by inserting documents into the system.auditing collection in the admin database.

Additionally, any authorization violation is always logged regardless of the audit rules.

Each audit rule has the following structure:

{
   "_id" : "<name of rule>",
   "collection_expr" : "<regex for selecting which collections to audit>",
   "database" : "<regex to select db name>",
   "username_expr" : "<regex for selecting which users to audit>",
   "actions" : [<action 1>,... <action n>],
   "on_sec_fail_only": false
}

The actions to be audited may contain any valid action, eg. insert/find/update/remove.

The on_sec_fail_only flag instructs SonarW to only log actions which fail authorization. By default actions are logged whether they pass or fail authorization.

Only users with the auditAdmin privileges can create or delete audit rules. The userAdmin role has been granted the auditAdmin role as well so your security administrator can also add/remove audit rules.

If you make any changes to the auditing rules the new rules will only apply to new session - i.e. existing open sessions are still audited based on rules before the change and any new session will use the modified rules.

To refresh/re-read the audit rules and have them take effect issue:

db.runCommand({readAuditData: 1})

If the audit.log file is deleted or you wish to clear the audit file and start anew, issue the following command in the admin database (need to be root):

db.runCommand({clearAudit:1})

The clearAudit command generates a __internal_auditClear_jsonar audit record in the audit file.

In addition to explicit rules, each login/logout and failed login is also audited.

The audit.log file is a CSV file with audit records such as:

1414438416,"rule1","insert","ron4","nva","flights","192.168.1.6:62350",0

Each line has the following entries:

  • Timestamp
  • Name of the rule that caused the entry to be written
  • Action performed (e.g. insert, find, LOGIN etc. Empty list means: all actions.)
  • Username
  • Collection name
  • Database name
  • Client IP and port of the connection
  • Return code - 0 means success, 1 means the action was not permitted

Actions supported for auditing:

- dropDatabaseAction
- hostInfoAction
- logRotateAction
- shutdownAction
- createCollectionAction
- createUserAction
- dropCollectionAction
- dropUserAction
- updateAuditDataAction
- createRoleAction
- viewRoleAction
- viewUserAction
- dropRoleAction
- grantRoleAction
- revokeRoleAction
- killCursorsAction
- killopAction
- auditFileClearAction
- findAction
- insertAction
- removeAction
- updateAction
- clearAudit

Data-Level Security (DLS)

SonarW’s data level security allows you to specify fine-grained access control rules on who can see what data. The mechanism is based on filtering rules that affect every query for every user - automatically and without the user being able to bypass this mechanism.

DLS rules are defined per database by adding them into the system.dls collection in that database. A DLS rule is defined per collection and optionally per user (or for all users when using user: ‘*’). A rule is a filter subdocument. This filter is automatically applied to every aggregation pipeline (as a $match) and to every find query (incuding the find element in an update). In an aggregation framework query this additional $match runs after the last $match in the pipeline but before any other non-$match stage (i.e. it is run as part of the $match-es in the beginning of the queries). It therefore filters out documents that the user is not entitled to see before the aggregation pipeline can change field names or document structures.

DLS rules are inserted into the system.dls collection in the database by the DLS administrator. A user must have the dlsAdmin role to edit or view the filters in system.dls.

Users that have the dlsExcluded roles are not affected by DLS rules and thus see all data.

SonarW allows a user to submit a query on behalf of another user. This allows a single connection (logged in as userA) to run queries in a “run-as” scenario and thus have the query filtered as if the query was run by userB. To do so, the user running the query must have the setUser role and needs to have a {$setUser: ‘userB’} stage at the start of the aggregation pipeline or have a {$setUser: ‘userB’} in the find query.

To debug DLS rules, turn on DLS debugging using:

db.runCommand({debugqueries:"DLS"})

The console will now show each query before and after the LDS rule is applied

Example

In the following example all collections have a field called “SonarG Source” and we want to use to control which user will see which records based on the source. As an example, a document in the collection buff_usage may look like:

> db.buff_usage.findOne()
{
     "_id" : ObjectId("56c876b0b34d033330346f01"),
     "UTC Offset" : -5,
     "Timestamp" : ISODate("2016-02-18T18:00:00Z"),
     ...
     "SonarG Source" : "gibm32",
     "gmachine" : "1762144738"
}

We have a control collection that lists which sources each user can see, e.g.:

> db.dls_source.find()
{ "_id" : ObjectId("57c8950ecb9f11020134a8ce"), "u" : "lmrm__ae", "SonarG Source" : "gibm32" }
{ "_id" : ObjectId("57c89ddc6e13784a3a9e9ab8"), "u" : "jane", "SonarG Source" : "gibm31" }
{ "_id" : ObjectId("57c89df56e13784a3a9e9ab9"), "u" : "jane", "SonarG Source" : "gibm33" }

And our DLS rule is:

> db.system.dls.findOne({collection: "buff_usage"})
{
     "_id" : ObjectId("57cd9cae9cabf447c8a6f136"),
     "collection" : "buff_usage",
     "user" : "*",
     "filter" : "{\"SonarG Source\":{\"$in\":{\"$ns\":\"dls_source\",\"$q\":{\"$expr\":{\"$eq\":[{\"$user\":1},\"$u\"]}},\"$p\":\"SonarG Source\"}}}"
}

Note that the filter is entered as a string since it may include $xyz as field names. If you enter rules using the shell you must ensure that bracketing is correct and it is recommended to use the JSON.stringify function, e.g:

db.system.dls.insert (
  {'collection':'dt',
    'user':'cale',
    'filter':  JSON.stringify(   {"salary":{"$gt":1000},"$expr":{"$in":[{"$user":1},["$ulist","$vlist"]]}}    )
} )

At this point every find and aggregation query will add the filter, in our case looking up the user in dls_sources and using the results within a subquery limiting the returned data. For example, if we run this $group aggregation for use lmrm__ae we get only the gibm32 data:

> db.buff_usage.aggregate({$group: {_id: "$SonarG Source", c: {$sum:1}}})
   { "_id" : "gibm32", "c" : 37627 }

If we trun on DLS debug the console shows the query rewrite that adds the $match:

2016-09-05T22:50:05.742633+0000 BackStore/SonarServer/find_cmd_execution.cc:490  [add_dls_filter] Applying dls filer for user lmrm__ae:
        query: {"aggregate":"buff_usage","pipeline":[{"$group":{"_id":"$SonarG Source","c":{"$sum":1}}}],"cursor":{}}
        filter: {"SonarG Source":{"$in":{"$ns":"dls_source","$q":{"$expr":{"$eq":[{"$user":1},"$u"]}},"$p":"SonarG Source"}}}

2016-09-05T22:50:05.742684+0000 BackStore/SonarServer/find_cmd_execution.cc:497  [add_dls_filter] Query with filter: {"aggregate":"buff_usage","pipeline":[{"$match":{"SonarG Source":{"$in":{"$ns":"dls_source","$q":{"$expr":{"$eq":[{"$user":1},"$u"]}},"$p":"SonarG Source"}}}},{"$group":{"_id":"$SonarG Source","c":{"$sum":1}}}],"cursor":{}}

When we run it using a user that has the dlsExcluded role we get the full data:

> db.buff_usage.aggregate({$group: {_id: "$SonarG Source", c: {$sum:1}}})
{ "_id" : null, "c" : 1061214 }
{ "_id" : "gmac03", "c" : 37112 }
{ "_id" : "gibm32", "c" : 37627 }
{ "_id" : "orna-vm01", "c" : 35445 }
{ "_id" : "gibm34", "c" : 35313 }
{ "_id" : "china2", "c" : 36548 }
{ "_id" : "gibm38", "c" : 36969 }

Finally, if the dls1 user has a role of setUser then we can run the query once as dls1, once as lmrm__ae and once as admin, and get the appropriate value each time (when running as dls1 the result set is empty vecayse dls1 does not have the dlsExcluded role and there are no records for dls1 in dls_sources (our control collection):

// As dls1 - since there is no setUser
> db.buff_usage.aggregate({$group: {_id: "$SonarG Source", c: {$sum:1}}})

// As admin, we see everything because admin has the dlsExcluded role
> db.buff_usage.aggregate({$setUser: 'admin'},{$group: {_id: "$SonarG Source", c: {$sum:1}}})
{ "_id" : null, "c" : 1061214 }
{ "_id" : "gmac03", "c" : 37112 }
{ "_id" : "gibm32", "c" : 37627 }
{ "_id" : "orna-vm01", "c" : 35445 }
{ "_id" : "gibm34", "c" : 35313 }
{ "_id" : "china2", "c" : 36548 }
{ "_id" : "gibm38", "c" : 36969 }

// As lmrm__ae
> db.buff_usage.aggregate({$setUser: 'lmrm__ae'},{$group: {_id: "$SonarG Source", c: {$sum:1}}})
{ "_id" : "gibm32", "c" : 37627 }

Schema-Level Security (SLS)

Schema-level security rules allow specifying which fields (columns) a user can see/use. SLS rules specify per collection and user which fields are readonly and which fileds cannot be seen at all. The rules are entered into the system.sls collection by the DLS administrator.

An entry in system.sls looks like:

{
   collection: collection-name ,
   user: user-name-or-* ,
   readonly: [ “filed1”,”field2”] ,
   block: [ “field3”, {“field4”: filter  }]
}

and means that for collection collection-name , if the user “user-name” or for any user if user-name is “*” :

  1. The field “field3” named in “block” will not show (or any of their subfields)
  2. The fields ‘field1’, ‘field2’ listed in “readonly” cannot be updated.
  3. The field named ‘filed4’ will only show if the expression given in ‘filter’ as ran on the original document processed by the query will not filter out the document.