Blog Post

Azure Data Explorer Blog
2 MIN READ

Row Level Security (RLS) - get more control over your data

slavikn's avatar
slavikn
Icon for Microsoft rankMicrosoft
Mar 01, 2020

We are excited to announce a new Azure Data Explorer (Kusto) feature: Row Level Security (RLS) policy, which gives you fine control over who can access data inside your tables. You can now prevent specific users from viewing certain rows in a table, and you can also mask the data they see. For example, you can set an RLS policy that masks personally identifiable information (PII), enabling developers to query production environments for troubleshooting purposes without violating compliance regulations.

 

Here are some other scenarios where RLS shines:

  • In a table named Sales, each row contains details about a sale. One of the columns contains the name of the sales person. Instead of giving your sales people access to all records in Sales, you can enable a Row Level Security policy on this table to only return records where the sales person is the current user.
  • If you have an AAD group that contains the managers of the sales people, you might want them to have access to all rows in the Sales table
  • If you have multiple AAD groups, and you want the members of each group to see a different subset of data, RLS lets you achieve this
  • A hospital can set an RLS policy that allows nurses to view data rows for their patients only
  • A bank can set an RLS policy to restrict access to financial data rows based on an employee's business division or role
  • A multi-tenant application can store data from many tenants in a single tableset (which is very efficient). They would use an RLS policy to enforce a logical separation of each tenant's data rows from every other tenant's rows, so each tenant can see only its own data.

There are only two simple steps to enable RLS on a table. Here’s how it works:

 

Suppose we have a table called Customers. When support representatives view it, we don’t want them to see the credit card numbers. So we create a Row Level Security policy to achieve that.

 

 

The first step is to create a function that will do the masking:

 

.create-or-alter function with () MaskCreditCardDataForSupportReps() {
let InSupportRep = current_principal_is_member_of('aadgroup=support_reps@mycompany.com');
let AllData = Customers | where InSupportRep != true;
let PartialData = Customers | where InSupportRep | extend CreditCardNumber = strcat("****-****-****-", substring(CreditCardNumber, strlen(CreditCardNumber)-4, 4)), Expiration = "**/**";
union AllData, PartialData
}
 
 
 

The second step is to enable the policy on the table:

 

.alter table Customers policy row_level_security enable "MaskCreditCardDataForSupportReps"

 

Now, when users access the Customers table, they’ll actually get the results of the MaskCreditCardDataForSupportReps function, which returns:

  • AllData: The full contents of Customers table, if the user is not a member of the support_reps@mycompany.com AAD Group
  • PartialData:  All rows from Customers table, with masked credit card details, if the user is a member of the support_reps@mycompany.com AAD Group

 

Row Level Security policy is now available for Public Preview. Find out more about it here: https://docs.microsoft.com/en-us/azure/kusto/management/rowlevelsecuritypolicy

 

UPDATE: Row Level Security policy is going to be Generally Available on Oct 14th, 2020.

Updated Sep 29, 2020
Version 6.0

8 Comments

  • Hi James,

     

    What you're trying to achieve sounds like a classic use case of the dynamic parameters features - it should allow you to bind filter/slicer visualizations to a parameter, which you can pass to an Azure Data Explorer function.

    I suggest you contact the Gabi Lehner <gabil(@)microsoft.com> and/or Ravi Kiran Vemulapalli <ravivem(@)microsoft.com> regarding the feature and it's availability.

    That being said, once again, I'm not sure if/when this will be supported for PBI Embedded.

     

    Itay.

  • jamesbdarcy's avatar
    jamesbdarcy
    Copper Contributor

    One more thing:

     

    I did wonder if the kusto function current_principal_details() might hold the key to retrieving the value passed in from EffectiveIdentity. I wondered if one of the following fields returned from current_principal_details() might be updated with the value:

     

    {
    "UserPrincipalName": "user@fabrikam.com",
    "IdentityProvider": "https://sts.windows.net",
    "Authority": "72f988bf-86f1-41af-91ab-2d7cd011db47",
    "Mfa": "True",
    "Type": "AadUser",
    "DisplayName": "James Smith (upn: user@fabrikam.com)",
    "ObjectId": "346e950e-4a62-42bf-96f5-4cf4eac3f11e",
    "FQN": null,
    "Notes": null
    }

    JBD

  • jamesbdarcy's avatar
    jamesbdarcy
    Copper Contributor

    Itay

     

    That looks very interesting and I will work with that to see where it leads.

     

    What if instead of a User Principal I want to return data based on a campaign Id. That is, I'm not interested in the current user as much as I am the current campaign. A user can have many campaigns but I'm only interested in one campaign at a time.

     

    For example, the Kusto stored function I am using from PBI might look like:

     

     

    .create-or-alter function with (docstring = 'blah', folder='-de-blah') 
    MyFunction(campaignId:long) // Note the parameters passed in from PBI { let advertisers = (campaignId:long) { Advertisers | where CampaignId == campaignId | project name; }; mytable | where TvcAdvertiserName in (advertisers(campaignId)) }

    But currently I have no way of passing the campaignId in dynamically from Power BI embedded.

     

    When I started thinking this through I thought what I needed was a function like the following, similar to how MS Analysis Services works:

     

    .create-or-alter function with (docstring = 'blah', folder='-de-blah') 
    MyFunction() // No parameter this time { let campaignId = <MagicCustomDataKustoFunction()>; // Value passed through securely from EffectiveIdentity instance when running as embedded let advertisers = (campaignId:long) { Advertisers | where CampaignId == campaignId | project name; }; mytable | where TvcAdvertiserName in (advertisers(campaignId)); }

     

     

    What you're suggesting won't allow me to perform additional lookups as shown in my examples above. Your suggestion depends on PBI adding additional 'where' clauses to columns that are not in the data set coming back.

     

    I feel like I'm just approaching this from the wrong angle.

     

    James

     

     

  • jamesbdarcy's avatar
    jamesbdarcy
    Copper Contributor

    Thanks Itay. I will look into that tomorrow. (it's evening here is Australia. Have a nice day

  • Here's the solution I got from one of our PBI experts (not sure it will work embedded):

    It may be possible to avoid using RLS, since the DAX function USERPRINCIPALNAME() works outside of RLS, but only as a measure. The idea is that:

     

    1. Load all user’s principal names into the model (in a separate table).
    2. Have a direct query Kusto function which returns all the data
    3. For each visual add a filter to it with: SELECTEDVALUE(Users[UserPrincipal]) = USERPRINCIPALNAME()
      1. This will cause the Kusto function to be modified by adding | where UserPrincipal == “<Filled in based on user viewing the report>”
    4. User sees and loads only their data.

    Another option might be the upcoming Dynamic Parameters feature that PBI are working on, that will allow you to bind a slicer/filter to a Parameter, thus affecting the data source query. Again, I'm not sure if/when it will work for Embedded PBI.

  • jamesbdarcy's avatar
    jamesbdarcy
    Copper Contributor

    Hi Itay

     

    Thanks for your reply.

     

    Can you explain the 'not easy' way to do what I am looking? A table full of users is not out of the question.

     

    SSO is not an option for embedded PBI users.

     

    If I could pass a parameter (a user id for example) from the PBI java script embedded environment through to the ADX Kusto query then I would.  However, to the best of my knowledge there is no way to update PBI data-source query parameters from the java script environment. To be as clear as possible, I know that you can update slicers and filters via the java script API and update filters via the URL parameters of the embed URL but neither of these mechanisms seem to allow the updating of data-source query parameters that would be sent through to the kusto query. I would imagine that this is probably not implemented due to data security concerns. 

     

    Make sure you have a look at https://docs.microsoft.com/en-us/power-bi/developer/embedded/embedded-row-level-security to see where I'm coming from. Note especially how Analysis services uses a method CUSTOMDATA() to access a value passed to it from the token created via EffectiveIdentity.CustomData property. I am imagining that would be the way in which ADX (kusto) would somehow access a value to perform row level filtering.

     

    JBD

  • Hi JBD,

     

    Currently, there's no easy way to do what you're trying to (it's feasible, but very under-performing, and requires having a table with the list of all possible users). We are in the process of supporting SSO for the ADX connector, which will allow you to use the report viewer's identity to query your ADX cluster. That being said, I'm not 100% sure this will work in a PBI Embedded scenario - it's possible you'll have to pass the user's identity to the query yourself.

     

    Itay.

  • jamesbdarcy's avatar
    jamesbdarcy
    Copper Contributor

    Hi All

     

    I would like to integrate Power BI RLS with ADX RLS. Would anyone have any ideas on how to achieve this?

     

    I can see how I can use the current_principal() and other methods within a kusto query to enable the RLS (https://docs.microsoft.com/en-us/azure/data-explorer/kusto/management/rowlevelsecuritypolicy). However, I am running Power BI in an embedded environment and therefore the current_principal() will always be the same. 

     

    Would any one know if it is possible to pick up a value passed in from a server side call to the PowerBIClient.Reports.GenerateTokenInGroupAsync method (or similar) and have that value picked up securely from within a Kusto stored function so that the value can be treated as a customer Id and be used as the row level security filter?

     

    Thanks in advance

     

    JBD