In this article I will look at a real-world example of data level security filtering.
Cognos does provide some role based security filtering capability.
I think the problem with roles is that there is usually an attribute associated with the role... and that on the other hand is not supported out-of-the-box.
Consider the following example, to undertsand what I mean by "attribute associated with the role"
Suppose we have the usual GO-Sales kind of data. We have a Sales measure and one of our dimensions is Branch.
We have two roles: CEO and Branch_Manager
Our requirement is that CEO can see everything but a Branch_Manager can only see Sales by his or her own branch.
This is the point where role-based security goes week.
Somehow we need to store which Branch a Branch_Manager is allowed to see.
It's not sufficient to know that a user has Branch_Manager role... we need to know (and handle) a BranchID associated with that role.
This is what I call the "attribute associated with the role" - in our example the BranchID.
Usually this implies we need to store this somewhere, eg in the database or maybe in a directory server like LDAP.
To implement data level security I find the best option is to use Slicers all over the place where cubes are referenced - in queries used by crosstabs and charts on the reports, in queries used to populate prompts on the filter pages.
(Keep in mind, I firmly believe that lists should not be used with a dimensional datasource.)
The slicer will make sure that filtering is applied to the entire cube, regardless of the dimensions used for display.
To continue with the example we need to write a slicer that
- figure out who the current user is
- what role the current user has - is the user a Branch_Manager
- what attribute is associated with that role - value of the BranchID
So what tools are in or arsenal to tackle this?
Session Parameters
Session parameters provide a way to get an identifier of the current user.
e.g.
account.userInfo |
Tamas Simon |
Session parameters can be used in macro expressions. The syntax is to prefix them with a ``$'' (dollar sign)
e.g.
#sq($account.userInfo)#
will result in
'Tamas Simon'
The exact details depend on how Cognos is configured...
Bottom line is that in every Cognos installation there should be some sort of identifier of the current user and it's accessible to report expressions using session parameters.
You can check the session parameters in Framework Manager by selecting Project / Session Parameters... from the menu.
Parameter Maps
Parameter maps are similar to session parameters in that they can be accessed from macro expressions.
They are name-value pairs, or rather "key-value" pairs - as Cognos calls it.
They are defined in the Framework Model.
They can be based on a query, just select a column for Key and another for Value.
Syntax is similar to session parameters.
``$'' (dollar sign) infornt of the name
``{}'' (curly braces) surrounding the key
``''' (single quote) surrounding the key if it is a string literal
e.g.
#$pmUserAccessableBranches{'Tamas Simon'}#
Here are some important notes I'd like to make:
1., There is no way to include or exclude parameter maps from a package. In my understanding when you re-publish a pckage all parameter maps are published with it.
2., The query subject that the parameter map is based on has to be published in the package. Otherwise you won't get any error or warning messages but the parameter map will be empty, always returning its default value if you have defined any.
I remember reading that the query is executed everytime an expression references the parameter map - but I have not confirmed this.
string aggregate function
3., Keys are unique. This is tricky... to continue our example imagine that we want to be able to assign multiple branches to the same user.
In other words we want to control which branches a user is allowed to access (maybe only one, maybe more)
So in the parameter map we need to have a list of IDs associated with a Key, all within a single string (stored as the Value)
e.g.
database table
UserName | BranchID |
Tamas Simon | B1 |
Tamas Simon | B2 |
Tamas Simon | B3 |
we would want the parameter map to look like
pmUserAccessableBranches
Key | Value |
Tamas Simon | B1 B2 B3 |
This is tricky considering that we need to write a query to populate the parameter map... but it's doable.
Oracle for example does not provide a string aggregate function out-of-the-box but one can write his own.
select UserName as Key, stragg(BranchID) as Value from UserAccessableBranches group by UserName
You can find a string aggregate function e.g. here: http://www.oracle-base.com/articles/10g/StringAggregationTechniques.php#user_defined_aggregate_function
With these three tools: session parameters, parameter maps based on DB query and string aggregation we can solve our data level filtering challenge.
We end up with slicers something like this:
filter( [Branches Dimension].[Branch], roleValue('_businessKey', [Branches Dimension].[Branch]) in ( #csv(split(' ', $pmUserAccessableBranches{$account.userInfo}))# ) )
from the inside out:
get the current user via session parameter: $account.userInfo
use this as a Key to look up the user accessable branches from the parameter map: $pmUserAccessableBranches{$account.userInfo}
massage the macro until it returns a comma separated string: #csv(split(' ', $pmUserAccessableBranches{$account.userInfo}))#
use the whole thing to filter branches... resulting in a slicer member set.
We could just generate a set() expression using the prompt...
set( #join(',', substitute('^', '[Branches Dimension].[Branch]->:[PC].[@MEMBER].[', substitute('$', ']', split(' ', $pmUserAccessableBranches{$account.userInfo}))))# )
This would work if the IDs are in synch with the cube. Otherwise we might get an error from Cognos, saying basically that we are refering to a member that does not exist. I find it safer to use the filtering expression.
A slicer filter will often not work if the same dimension appears in the crosstab (or chart) elsewhere.
ReplyDeleteIn this case use a detail filter instead.
This comment has been removed by the author.
ReplyDeleteHey Tamas,
ReplyDeleteGreat website. Very useful information.
I couldn't get your below expression to work because the substitute macro only replaces the first instance of the argument.
set( #join(',', substitute('^', '[Branches Dimension].[Branch]->:[PC].[@MEMBER].[', substitute('$', ']', split(' ', $pmUserAccessableBranches{$account.userInfo}))))# )
So instead, I used the split macro followed by a join macro to do substitutions. Here is my code (which does work):
set(#join('[COBRA_TEST].[CCN].[CCN].[CCN1]->:[PC].[@MEMBER].[',split(';',';'+join('],;',split(';',promptmany('ccn','token','263078122890')))))+']'#)
(Explanation: first I replace the semicolons with '],;' and then I replace the new semicolons with the beginning of the MUN construct.)
Is there a shorter way of doing the same thing? I feel like I'm using brute force more than finesse.
Thanks for your help,
Bob
Hi Bob
ReplyDeleteI don't know any other way either.
Seems like there is no way to force the substutute() macro to go beyond the first occurrence.
In reports I worked on I did just the same as you.
hotmail login
ReplyDeleteI have you bookmarked to check out new stuff of your blog a must read blog!
Great insights. I look forward to reading what you're planning on next, because your post is a nice read.
ReplyDeletekajal agarwal hot