How to Audit Read Access to Data in Caché

How to Audit Read Access to Data in Caché.
Introduction

Since the introduction of the Caché 5.1+ security model, it has become much easier to both secure access to Caché data and to audit updates to that data. However, there is still no generalized mechanism for auditing read access to data held in a Caché database. There are times when this is a strict requirement, e.g. HIPAA, financial data, etc. This document shows how to achieve such auditing and to close off any access methods that would seek to bypass such mechanism. Much of the approach that is discussed can be applied to 5.0, as an application level auditing mechanism, but it would be much more difficult – perhaps impossible – to achieve the same level of security.

Possible Access Methods

Caché’s Universal Data Architecture means that data is generally accessed in one of three ways: Object, SQL or Global. Since data is held, at the file system level, in a Cache.dat file, which is not human readable, then this is not considered to be a viable access mechanism that could be exploited. Encrypting the database makes it practically impossible to access data in this way. In the next sections, we will discuss each of the access methods and how to secure them and how to make sure that any authorized access to sensitive data is fully audited.

Securing Access

Database and Global Mapping

Object

Any property in a Caché class can be accessed via an instance of that class, unless the property has been defined as "Private". When the data is accessed it is via accessor methods that are generally not implemented explicitly within a class definition. The developer of the class may override the setter and getter methods; if this is done, then all object based access to the instance’s property will invoke a call to the appropriate method.

So, if I want to make sure that object based access to one of my properties is audited, I can simply override one or both of the getter and setter methods, and include a call to the auditing mechanism. (See example for further details.)

Note the use of the i% variable to avoid recursive calls within the methods.

Method Get as %String { do $system.Security.Audit(......) quit i% } Method Set(value as %String) as %Status { do $system.Security.Audit(......) set i% = value quit $$$OK }

SQL

SQL is a separate access mechanism, so we need to ensure audited access via a different approach. If we only needed to audit updates, we could use SQL Triggers as the driver, but there is no trigger for SQL Select, and reading of data needs to be audited as well. So, we use a slightly different approach:

We make the "real" auditable field a private property. The property we are going to expose to the user, via SQL is Caculated and SQLComputed. The SQLComputeCode for the property includes a call to the getter for the “real” property which, as we saw above, includes a call to the Audit method.

However this is not sufficient to ensure that SQL access to the “real” property will always be audited. Marking the property as private means that it does not show up in “select *” results, for instance, but it is still fully visible in the Table Schema definition and can be referred to explicitly in a select statement, thus bypassing the audit call.

The final step is to create an updateable view of the table that includes all columns except the “real” auditable column. Then we grant SQL privileges on the view to the appropriate set of users and close down SQL access to the base table.

Global

Global access bypasses any defences set up in the object and SQL aspects. Until row level security is implemented - and depending on how it is implemented - we need to rely on database level granularity to protect access to globals. That is, we can prevent users from accessing the database at all. This would mean that, for instance, a user who had access to the Caché terminal could not just zn to the appropriate namespace and do a %G to see the contents of the global. It also means that they would not have access to any of the data in that database. A suggested approach would be to have a database whose sole purpose was to hold the sensitive data and use global mapping to push sensitive data in a particular namespace into that database, but leave the rest of the namespace’s data in the original, default database. (Given that this is, by definition, sensitive data, we would probably want to encrypt this database.) The sensitive database would be protected by a unique resource that is not allocated to any user. This would prevent ad hoc access from the terminal or other, similar approaches. For applications that needed to access the database, dynamically adding roles could be a viable solution. However, this generally only applies to CSP applications and executables, so it is not clear to me how this could be used in a java context.

Another option is to set up all the application users in such a way that they only have access to the information via the application. This means a role that contains %Service_SQL (to allow access via xDBC) and the specific resources that protect the database(s) in question. The role will also need the appropriate SQL privileges to access the tables. If users also require access via terminal, for instance, this would be a hole in the security mechanism, allowing them direct view on the global data, bypassing any auditing. One solution would be to have separate userids for those users that definitely need other forms of access. These userids would not have access to the resource protecting the sensitive database, but would have terminal access, etc.

Auditing Access

The actual auditing of data access can be achieved by putting in calls to $system.Security.Audit in the appropriate getter and setter methods.

Example

The example below illustrates all of the points above. The “CreateView” ClassMethod is included simply to help create the view for testing. In order to use this class, User Audit events need to be created: "CreditCardAudit/Info/WriteAccess" and "CreditCardAudit/Info/ReadAccess"

Class User.Auditable Extends (%Persistent, %Populate) { Property CCNumber As %String [ Calculated, SqlComputeCode = { set {CCNumber}=##class(User.Auditable).CalcCC({ID}) }, SqlComputed ]; Property Name As %Name; Property CCNo As %String(POPSPEC = "##class(User.Auditable).CCNoGen") [ Private ]; Method CCNumberGet As %String { 	do ##class(%SYSTEM.Security).Audit("CreditCardAudit","Info","ReadAccess",..CCNo) quit ..CCNo } Method CCNumberSet(value As %String) As %Status { 	do ##class(%SYSTEM.Security).Audit("CreditCardAudit","Info","WriteAccess",value) set ..CCNo = value quit $$$OK } ClassMethod CCNoGen As %String { 	set CCNo = "" for i=1:1:10 { set randInt = $r(10)+1 set CCNo = CCNo_randInt }	 	quit CCNo } ClassMethod CalcCC(ID As %Integer) As %String { 	do $system.Security.Audit("CreditCardAudit","Info","ReadAccess","CalcCC") quit ##class(User.Auditable).%OpenId(ID).CCNumberGet } ClassMethod CreateView As %Status { 	do $system.SQL.Login("_system","SYS") do $system.Security.Login("_system","SYS") set rs = ##class(%Library.ResultSet).%New //Create a string containing a DDL command set ddl = "DROP VIEW AuditableView" w !, ddl //Prepare and Execute the DDL command set rc = rs.Prepare(ddl) if ('rc) do $system.OBJ.DisplayError(rc) set rc = rs.Execute if ('rc) do $system.OBJ.DisplayError(rc) do rs.Close kill rs 	set rs = ##class(%Library.ResultSet).%New //Create a string containing a DDL command set ddl = "CREATE VIEW AuditableView " set ddl = ddl_"AS SELECT Name, CCNumber FROM Auditable" w !, ddl //Prepare and Execute the DDL command set rc = rs.Prepare(ddl) if ('rc) do $system.OBJ.DisplayError(rc) set rc = rs.Execute if ('rc) do $system.OBJ.DisplayError(rc) quit rc } }