Article Vitaliy Serdtsev · Jun 14, 2017 3m read

Dynamic row-level security

This is a translation of the following article. Thanks [@Evgeny Shvarov] for the help in translation.

Someone posted a question on DC asking whether it was possible to determine access rights for a particular table row always at runtime, and if it was, how could one do that?
Answer: it is possible and it’s not hard at all.

Here’s what you need to do:
  • stop hanging out on DC only :)
  • disable the %RLI index on the SQL engine level (detailed instructions can be found here) to avoid doing this in every query using %IGNOREINDEX
  • make the %READERLIST field always calculated.
    Note: If you want to, you can “zero out” access rights during each calculation triggered by an INSERT/DELETE operation, which is defined by the %oper variable. For obvious reasons, it works for SQL only.
Let’s create the following class to demonstrate different approaches:
Class demo.test Extends %Persistent
{

Parameter ROWLEVELSECURITY = 1;

Property %READERLIST As %String CalculatedPrivateSqlComputeCode = {{*} $s(%oper="":"%All",$$$YES:$$$SQLEmptyString)}, SqlComputed ];

Property f1 As %String;

Property f2 As %Integer;

/// d ##class(demo.test).Fill() ClassMethod Fill(3) {   ..%KillExtent()   i=1:1:&sql(insert into demo.test(f1,f2values('f'||:i,:i))      zw ^demo.testD,^demo.testI !      ..Test() }

ClassMethod Test() {   i=$$$YES,$$$NO {     d $SYSTEM.SQL.SetMapSelectability($classname(),"%RLI",i), $system.SQL.PurgeForTable($classname())          w $s(i=$$$YES:"yes",1:"no")" 1) -----",!!     ##class(%SQL.Statement).%ExecDirect(,"select * from %IGNOREINDEX %RLI "$classname()).%Display() !!     w $s(i=$$$YES:"yes",1:"no")" 2) -----",!!     ##class(%SQL.Statement).%ExecDirect(,"select * from "$classname()).%Display() !!   } }

}

Now, let's use the terminal to run the Fill method as a user with the %All role:
USER>##class(demo.test).Fill()
^demo.testD=3
^demo.testD(1)=$lb("","f1",1)
^demo.testD(2)=$lb("","f2",2)
^demo.testD(3)=$lb("","f3",3)
^demo.testI("$test",1)=$zwc(412,1,0)/$bit(2..4)/
^demo.testI("%RLI"," "_$c(0),1)=$zwc(412,1,0)/$bit(2..4)/

yes 1) -----

ID f1 f2 1 f1 1 2 f2 2 3 f3 3

3 Rows(s) Affected

yes 2) -----

ID f1 f2

0 Rows(s) Affected

no 1) -----

ID f1 f2 1 f1 1 2 f2 2 3 f3 3

3 Rows(s) Affected

no 2) -----

ID f1 f2 1 f1 1 2 f2 2 3 f3 3

3 Rows(s) Affected

PS: it goes without saying that in a real project, the calculation procedure will be a bit more complex than the banal %All, and may include several roles/logins separated with a comma.

Comments

Kyle Baxter · Jun 14, 2017

I think it is important to remember that Row Level Security typically results in worse performance.  It is better to create a VIEW and give your users access to the VIEW instead of going through this exercise.  

0