All posts in “ba-server”

Securing a Pentaho Server

How to customise security and access

This post will show you how you can restrict the access to your Pentaho BA Server, essentially creating a “guest” user role, and cherry picking what that user has access to.

Spring Security – Becoming very selective

We want to restrict the access of visitors to only specific demos, rather than the default permissions granted to the Authenticated users.

In order to do this we first need to adjust our BA Server’s Administration->User/Roles settings. What we want to achieve is a specific role for all company users, and a set of separate roles for visitors.

First we create a role for company users with almost all privileges, and remove all other user roles. After this is done we will need to restrict the privileges of Authenticated users. We restrict their access to a minimum so we only allow them to read and execute content.

Now we add a guest user, without a role associated, which means it will behave as an Authenticated user. Once this is done we need to hard code how access to web services are given to internal roles. Our specific goal in this post will be to allow a guest user access only to a single demo dashboard.

Opening Spring Framework’s “pentaho-solutions/system/applicationContext-spring-security.xml”, you will see that this file contains the configuration of the security filters for various services. Specifically we will be working on the “filterInvocationInterceptorForWS” and “filterInvocationInterceptor” beans, which grant access to specific user roles to webservices which are accessed through URI’s. Each one of these beans lists a set of patterns and the roles that have access to them. The first step is to restrict all access only for company users. Then add patterns that will be regex-matched to cherry picked dashboards and allow any Authenticated user access. 

By default this is what filterInvocationInterceptor looks like:

<bean id="filterInvocationInterceptor" class="org.springframework.security.web.access.intercept.FilterSecurityInterceptor">
<sec:filter-security-metadata-source request-matcher="ciRegex" use-expressions="false">
<sec:intercept-url pattern="\A/content/common-ui/resources/web/(.+/)*.+\.js.*\Z" access="Anonymous,Authenticated" />
<sec:intercept-url pattern="\A/.*require-cfg.js.*\Z" access="Anonymous,Authenticated" />
<sec:intercept-url pattern="\A/.*require-js-cfg.js.*\Z" access="Anonymous,Authenticated" />
<sec:intercept-url pattern="\A/content/common-ui/resources/web/require.js.*\Z" access="Anonymous,Authenticated" />
<sec:intercept-url pattern="\A/content/common-ui/resources/web/require-cfg.js.*\Z" access="Anonymous,Authenticated" />
<sec:intercept-url pattern="\A/content/data-access/resources/gwt/.*css\Z" access="Anonymous,Authenticated" />
<sec:intercept-url pattern="\A/webcontext.js.*\Z" access="Anonymous,Authenticated" />
(...)

A service granted to both Authenticated and Anonymous will refer to a service required for any access (including prior to logging in), we must allow those to remain open as they are essential. We therefore focus on patterns that are granted only to Authenticated users and edit the “access” to list only our role “User” and “Admin”.

<sec:intercept-url pattern="\A/mantle/.*\Z" access="User,Admin" />
<sec:intercept-url pattern="\A/.*\Z" access="User,Admin" />

This security filter manages only basic BA Server components, like for instance the login services. However does not filter access to the home screen. Which means a user that is Authenticated has no access to mantle, however will be shown an empty homepage. In order to avoid this we add a pattern for /home, also restricted to our role “User” and “Admin”. It is important to note that these security beans are applied from top to bottom, which means order matters very much. We add our new pattern immediately after the mantle one. A snippet should look like the one below:

<sec:intercept-url pattern="\A/mantlelogin/.*\Z" access="Anonymous,Authenticated" />
<sec:intercept-url pattern="\A/mantle/mantleloginservice/*\Z" access="Anonymous,Authenticated" />
<sec:intercept-url pattern="\A/mantle/.*\Z" access="User,Admin" />
<sec:intercept-url pattern="\A/home.*\Z" access="User,Admin"/>

<sec:intercept-url pattern="\A/welcome/.*\Z" access="Anonymous, Authenticated" />
<sec:intercept-url pattern="\A/public/.*\Z" access="Anonymous,Authenticated" />
<sec:intercept-url pattern="\A/login.*\Z" access="Anonymous,Authenticated" />

Now we turn to the filterInvocationInterceptorForWS and follow the same first step, restrict the access to all patterns granted to Authenticated changing them to only to “User, Admin”.

<sec:filter-security-metadata-source request-matcher="ciRegex" use-expressions="false">
<sec:intercept-url pattern="\A/webservices/unifiedrepository\?wsdl.*\Z" access="Anonymous,Authenticated" />
<sec:intercept-url pattern="\A/webservices/userrolelistservice\?wsdl.*\Z" access="Anonymous,Authenticated" />
<sec:intercept-url pattern="\A/webservices/userroleservice\?wsdl.*\Z" access="Anonymous,Authenticated" />
<sec:intercept-url pattern="\A/webservices/authorizationpolicy\?wsdl.*\Z" access="Anonymous,Authenticated" />
<sec:intercept-url pattern="\A/webservices/rolebindingdao\?wsdl.*\Z" access="Anonymous,Authenticated" />
<sec:intercept-url pattern="\A/webservices/scheduler\?wsdl.*\Z" access="Anonymous,Authenticated" />
<sec:intercept-url pattern="\A/webservices/repositorysync\?wsdl.*\Z" access="Anonymous,Authenticated" />
<sec:intercept-url pattern="\A/webservices/datasourcemgmtservice\?wsdl.*\Z" access="Anonymous,Authenticated" />
<sec:intercept-url pattern="\A/webservices/.*\Z" access="User,Admin" />
<sec:intercept-url pattern="\A/plugin/reporting/api/jobs/.*\Z" access="Anonymous,Authenticated" method="OPTIONS" />
<sec:intercept-url pattern="\A/plugin/reporting/api/jobs/.*\Z" access="Anonymous,Authenticated" method="HEAD" />
<sec:intercept-url pattern="\A/api/repos/.*\Z" access="Anonymous,Authenticated" method="OPTIONS" />
<sec:intercept-url pattern="\A/api/repos/.*\Z" access="Anonymous,Authenticated" method="HEAD" />
<sec:intercept-url pattern="\A/api/.*require-cfg.js.*\Z" access="Anonymous,Authenticated" />
<sec:intercept-url pattern="\A/api/.*require-js-cfg.js.*\Z" access="Anonymous,Authenticated" />
<sec:intercept-url pattern="\A/api/.*\Z" access="User,Admin" />
<sec:intercept-url pattern="\A/plugin/.*\Z" access="User,Admin" />

So at this stage we have restricted all access to almost all components and services to our company users using the role “User”. And we are now able to cherry pick the exact services we want to open. In order to allow access by visitors to our dashboard and nothing else, we add a URI pattern and allow access to “Authenticated”. We then add it on the TOP, which means it is the first applied filter and therefore the most constricting.

<sec:intercept-url pattern="\A/api/repos/:public:ubiquis:world_population:world_population.wcdf/generatedContent.*\Z" access="Authenticated"/>

At this stage if you try to open the URI the dashboard will fail to render. This happens because the visitor is trying to render the dashboard without access to any other services, so through trial and error use the browser console and check which other services are required to render that particular dashboard. Once you identify which ones are required you open those services too.

<sec:intercept-url pattern="\A/api/repos/:public:ubiquis:world_population:world_population.wcdf/generatedContent.*\Z" access="Authenticated"/>
<sec:intercept-url pattern="\A/api/repos/:public:ubiquis:world_population:.*\.png*\Z" access="Authenticated"/>
<sec:intercept-url pattern="\A/plugin/cda/api/doQuery\?.*\Z" access="Authenticated"/>
<sec:intercept-url pattern="\A/api/repos/pentaho-cdf/.*\Z" access="Authenticated"/>
<sec:intercept-url pattern="\A/api/repos/pentaho-cdf-dd/.*\Z" access="Authenticated"/>
<sec:intercept-url pattern="\A/plugin/pentaho-cdf-dd/api/resources/public/ubiquis/.*\Z" access="Authenticated"/>
<sec:intercept-url pattern="\A/webservices/unifiedrepository\?wsdl.*\Z" access="Anonymous,Authenticated" />
<sec:intercept-url pattern="\A/webservices/userrolelistservice\?wsdl.*\Z" access="Anonymous,Authenticated" />
<sec:intercept-url pattern="\A/webservices/userroleservice\?wsdl.*\Z" access="Anonymous,Authenticated" />
<sec:intercept-url pattern="\A/webservices/authorizationpolicy\?wsdl.*\Z" access="Anonymous,Authenticated" />
<sec:intercept-url pattern="\A/webservices/rolebindingdao\?wsdl.*\Z" access="Anonymous,Authenticated" />
<sec:intercept-url pattern="\A/webservices/scheduler\?wsdl.*\Z" access="Anonymous,Authenticated" />
<sec:intercept-url pattern="\A/webservices/repositorysync\?wsdl.*\Z" access="Anonymous,Authenticated" />
<sec:intercept-url pattern="\A/webservices/datasourcemgmtservice\?wsdl.*\Z" access="Anonymous,Authenticated" />
<sec:intercept-url pattern="\A/webservices/.*\Z" access="User,Admin" />
<sec:intercept-url pattern="\A/plugin/reporting/api/jobs/.*\Z" access="Anonymous,Authenticated" method="OPTIONS" />
<sec:intercept-url pattern="\A/plugin/reporting/api/jobs/.*\Z" access="Anonymous,Authenticated" method="HEAD" />
<sec:intercept-url pattern="\A/api/repos/.*\Z" access="Anonymous,Authenticated" method="OPTIONS" />
<sec:intercept-url pattern="\A/api/repos/.*\Z" access="Anonymous,Authenticated" method="HEAD" />
<sec:intercept-url pattern="\A/api/.*require-cfg.js.*\Z" access="Anonymous,Authenticated" />
<sec:intercept-url pattern="\A/api/.*require-js-cfg.js.*\Z" access="Anonymous,Authenticated" />
<sec:intercept-url pattern="\A/api/.*\Z" access="User,Admin" />
<sec:intercept-url pattern="\A/plugin/.*\Z" access="User,Admin" />

As you can see there were images that needed to be accessed in our repository, and we also needed to allow the user to run CDA queries. There are a number of javascript and css files also required for CDF to render our dashboard correctly. Notice that although we allowed access to CDA, we did not grant access to the CDA previewer or editor.

The resulting dashboard can be found clicking this link (you can see that username and password are being passed in the URL).

This principle can be applied to different guest user roles, where a subset of visitors will have access to one part of the server, and others another, allowing for a much more controlled approach to how to grant access to your server.

 

 

Reading/Writing files from a Kettle CDA datasource in Pentaho 5.x

Kettle datasources in CDA are the most versatile way of getting data from disparate systems into a dashboard or report. Both C-tools and PRD expose Kettle as a datasource, so we can call a transformation that queries several different systems (databases, web services, etc.), do some Kettle magic on the results and then ship the final resultset back to the server.

However… there’s no way out of the box to reference sub-transformations in Pentaho 5.x. Solution files are now hosted on Jackrabbit and not on the file system and ${Internal.Transformation.Filename.Directory} just doesn’t work. If you have a transformation calling a sub-transformation and upload both to the repository, when you run it you get an error message saying that it cannot find the file.

Although this usually isn’t a problem, as most Kettle transformations used by dashboards are relatively simple, for more complex tasks you may want to use mappings (sub-transformations), call a job or transformation executor or, and perhaps the best use case, use metadata injection. In these scenarios you _really_ need to be able to call ktr files from the BA server context, and as it happens you can’t.

Well, sort of. As it turns out, you can.

The problem: lets say you have your server installed in /opt/pentaho/biserver-ce (or biserver-ee, we at Ubiquis don’t discriminate), and your kettle file is in your repository in /public/myDashboard/files/kettle/myTransformation.ktr. You want to use Metadata injection on a transformation called myTemplate.ktr and so you reference it by ${Internal.Transformation.Filename.Directory}/myTemplate.ktr in your metadata injection step. When you run your datasource, your error message will say something like

Cannot read from file /opt/pentaho/biserver-ce/pentaho-solutions/public/myDashboard/files/kettle/myTemplate.ktr because it’s not a file.

So what seems to be happening is that Kettle is using your pentaho-solutions folder as the root folder and it just appends the JCR path to it. Which obviously fails, as there’s no physical counterpart in your filesystem for the JCR files.

But, if you use the Pentaho Repository Synchronizer plugin to copy the contents of JCR to the filesystem and vice-versa (which is perhaps the simplest way to have your solution files under GIT or SVN, at least until the Ivy GS plugin becomes fully functional), then the synchronisation process will create a folder repositorySynchronizer inside your pentaho-solutions folder and use it to create the copies of every JCR file.

So, after synchronising JCR with the file system, your template transformation can be found in /opt/pentaho/biserver-ce/pentaho-solutions/repositorySynchronizer/public/myDashboard/files/kettle.

And now, the funny bit: to get it working, just create a symbolic link to remove the extra repositorySynchronizer element from your path: go to your pentaho-solutions folder and just type ln -s repositorySynchronizer/public public.

Now the template transformation is found and you can reference it from a transformation called by a dashboard.

Credit for the discovery goes to Miguel Cunhal, our junior consultant, that had the tough task of getting our metadata injection transformation running inside a demo dashboard. But more on that in another post.