Web Services Security 2.x-3.0.x
Tip: See Authentication from Third-Party Applications for examples of web services clients.
Authentication and Web Resource Authorization of Web Services Clients
Background
The ultimate destination of resources provided by the Pentaho BI Server is your users. Those resources can be consumed directly using a web browser or indirectly using web services. In the former case, the user has explicitly requested the resource. In the latter case, code has requested the resource (on behalf of the user).
The authentication process differs when dealing with a web browser as opposed to a web service client. Why? To find out, let's look at the diagram below.
In the above diagram, we see a Pentaho service at the lowest layer. In this context, a Pentaho service is any Pentaho BI Server code that one wishes to execute remotely. One layer above the service, we see two authentication mechanisms--one for web service clients and one for web browsers. At the top of the diagram, there is a server icon representing a web service client and a workstation icon representing a web browser request. Both make requests, as indicated by request labels the down arrows. Both receive the resource that they requested if all goes well, as indicated by the resource labels on the up arrows. But what happens when things don't go well? If the BI Server knows who you are yet still decides to deny access, the requester will receive a forbidden response. Forbidden is HTTP response code 403. What about when the BI Server knows who you are?
If you'll notice, there's only one thing different between the interactions observed from web browsers and web service clients--the returned value when the BI Server doesn't yet know who you are. Why do they differ? Well, one response is best processed by a human, as indicated by the redirect (to the login page). And another response is best processed by code, as indicated by the auth req. The auth req, or "authentication request" is HTTP response code 401.
Technical Information: What happens as a result of submitting bad credentials (e.g. an unknown username and/or incorrect password)? In digest authentication, the response code is still 401. In form authentication, the login page is redisplayed, this time with an error message. The response code in this case is 200 (OK).
So web service clients can easily know (by checking for response code 401) whether or not the BI Server recognizes them. Why can't it just check for response code 302 (a redirect)? A redirect is generic and used in other contexts besides authentication. So its presence alone is not enough. Even if the web service client is smart enough to follow a redirect (and re-request the target specified in the redirect instruction), the login page will be returned as the resource requested and the web service client will be unable to determine this.
Acegi Security Configuration Examples
The examples below show different ways of configuring the Pentaho BI Server for secure web services along with client-side code examples.
Example using Basic
Basic authentication is enabled out-of-the-box. However, the server, as configured, will never send a challenge (response code 401). That's because the Basic authentication processing mechanism is initiated only when credentials are supplied. Why? Because the Pentaho BI Server is also guarded using Form Authentication so the absence of Basic credentials is assumed to be a human client. Using this setup, the way to get your credentials processed is to send your request with credentials without waiting for the challenge.
// assume url, username, and password not null in this example HttpClient client = new HttpClient(); PostMethod postMethod = new PostMethod(url.toExternalForm()); Credentials creds = new UsernamePasswordCredentials(username, password); client.getState().setCredentials(AuthScope.ANY, creds); client.getParams().setAuthenticationPreemptive(true); // send creds without prompt! try { int status = client.executeMethod(postMethod); if (status == HttpStatus.SC_OK) { String postResult = postMethod.getResponseBodyAsString(); } } catch (Exception e) { e.printStackTrace(); } finally { postMethod.releaseConnection(); }
Example using Digest and Partitioned URLs
Ideally, there is one set of URLs that are requested by humans and another set of URLs requested by code. In this example, this partitioning is shown.
Filter Chain
Because we want an authentication mechanism depending on the client type (web browser or web service client), we define two filter chains. (What is implied here is that all web service calls live under /webservices/
.) Notice that the filter chains are quite different yet share some common filters between them. We'll see that some filters can be shared between the two chains and others cannot.
Warning: Note that the end-of-line backslashes that occur in the excerpt below are present for formatting purposes only and should not be present in the actual file.
<bean id="filterChainProxy" class="org.acegisecurity.util.FilterChainProxy"> <property name="filterInvocationDefinitionSource"> <value> <![CDATA[CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON PATTERN_TYPE_APACHE_ANT /webservices/**=securityContextHolderAwareRequestFilterForWS,httpSessionContextIntegrationFilter, \ digestProcessingFilter,anonymousProcessingFilter,exceptionTranslationFilterForWS,filterInvocationInterceptorForWS,pentahoSecurityStartupFilter /**=securityContextHolderAwareRequestFilter,httpSessionContextIntegrationFilter,httpSessionReuseDetectionFilter,logoutFilter, \ authenticationProcessingFilter,basicProcessingFilter,requestParameterProcessingFilter,rememberMeProcessingFilter,anonymousProcessingFilter, \ pentahoSecurityStartupFilter,switchUserProcessingFilter,exceptionTranslationFilter,filterInvocationInterceptor]]> </value> </property> </bean>
HttpSessionContextIntegrationFilter
From the Javadoc for HttpSessionContextIntegrationFilter
:
If for whatever reason no
HttpSession
should ever be created (eg this filter is only being used with Basic authentication or similar clients that will never present the samejsessionid
etc), thesetAllowSessionCreation(boolean)
should be set tofalse
.
And another quote from the Acegi Security documentation stating something similar:
As web services will never present a jsessionid on future requests, creating
HttpSession
instances for such user agents would be wasteful. If you had a high-volume application which required maximum scalability, we recommend you use the approach shown [in this section of the documentation]. For smaller applications, using a singleHttpSessionContextIntegrationFilter
(with its defaultallowSessionCreation
astrue
) would likely be sufficient.
While Acegi Security recommends setting allowSessionCreation
to false
for web services, the example on this page shares a single HttpSessionContextIntegrationFilter
(with allowSessionCreation
as true
) for reasons of simplicity.
Expose the Web Services
Shown below is a single Pentaho service. It is a servlet that is exposed via two URLs. One is applicable for browsers (the first one) and one is applicable for web service clients (the second one). Note that both entry points (URLs) are protected; they each have a filter chain that is applicable.
<servlet> <servlet-name>ViewAction</servlet-name> <servlet-class>org.pentaho.ui.servlet.ViewAction</servlet-class> </servlet> <servlet-mapping> <servlet-name>ViewAction</servlet-name> <url-pattern>/ViewAction</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>ViewAction</servlet-name> <url-pattern>/webservices/ViewAction</url-pattern> </servlet-mapping>
Warning: The order of the filters in the filter chains is imperative! You might notice that the filter
pentahoSecurityStartupFilter
occurs afterfilterInvocationInterceptorForWS
in the first filter chain but afterfilterInvocationInterceptor
in the second filter chain. It could in fact be listed afterfilterInvocationInterceptorForWS
andfilterInvocationInterceptor
respectively!The reason that
pentahoSecurityStartupFilter
must come afterfilterInvocationInterceptorForWS
is this:DigestProcessingFilter
populates theSecurityContext
with anAuthentication
instance but does not set is to authenticated. That is done later inFilterSecurityInterceptor.beforeInvocation()
. This is also when roles are fetched. And until roles are fetched, we don't wantpentahoSecurityStartupFilter
to run. (In the second filter chain,pentahoSecurityStartupFilter
can precedefilterInvocationInterceptor
sinceauthenticationProcessingFilter
fetches the roles for the user.
Digest Authentication
The example below uses digest authentication.
<bean id="digestProcessingFilter" class="org.acegisecurity.ui.digestauth.DigestProcessingFilter"> <property name="userDetailsService"><ref bean="userDetailsService"/></property> <property name="authenticationEntryPoint"><ref local="digestProcessingFilterEntryPoint"/></property> <property name="userCache"><ref local="userCache"/></property> </bean> <bean id="digestProcessingFilterEntryPoint" class="org.acegisecurity.ui.digestauth.DigestProcessingFilterEntryPoint"> <property name="realmName"><value>Pentaho Realm</value></property> <property name="key"><value>p5nt5h5</value></property> <property name="nonceValiditySeconds"><value>90</value></property> </bean>
Exception Translation
<bean id="exceptionTranslationFilterForWS" class="org.acegisecurity.ui.ExceptionTranslationFilter"> <property name="authenticationEntryPoint"> <ref local="digestProcessingFilterEntryPoint" /> </property> <property name="accessDeniedHandler"> <bean class="org.acegisecurity.ui.AccessDeniedHandlerImpl" /> </property> </bean>
Inject User into HTTP Servlet Request
The SecurityContextHolderAwareRequestFilter
takes an Authentication
instance (populated by Acegi Security) and exposes that Authentication
instance via Servlet API calls such as getRemoteUser()
and getUserPrincipal()
. This filter is necessary since SecurityStartupFilter
, a custom Pentaho filter, depends on the aforementioned Servlet API calls.
SecurityContextHolderAwareRequestFilter
makes use of wrappers. There are two wrappers provided by Acegi Security: SecurityContextHolderAwareRequestWrapper
and SavedRequestAwareWrapper
. Note that the subclass of SecurityContextHolderAwareRequestWrapper
, SavedRequestAwareWrapper
, is not appropriate for web service clients! It attempts to save the original request made by the client for recalling later after authentication. This is bad for two reasons. For one, using the SavedRequestAwareWrapper
adversely affects web service calls (see information box below). And for another, the request and the login are part of the same request, so saving the request is unnecessary.
Technical Information:
UsingSavedRequestAwareWrapper
adversely affects web service calls. Why? Consider the following example. User requests a resource without Authorization header (a header required to authenticate via digest authentication). Server stores request and responds with 401. User is prompted with dialog for username and password. User enters username and password and submits. Server pulls saved request. DigestProcessingFilter asks request for Authorization header. Since request is the original request, no Authorization header is found and the user is prompted with dialog again.
<bean id="securityContextHolderAwareRequestFilterForWS" class="org.acegisecurity.wrapper.SecurityContextHolderAwareRequestFilter"> <property name="wrapperClass" value="org.acegisecurity.wrapper.SecurityContextHolderAwareRequestWrapper" /> </bean>
Filter Security Interceptor
Web services URLs are protected with different access rules.
<bean id="filterInvocationInterceptorForWS" class="org.acegisecurity.intercept.web.FilterSecurityInterceptor"> <property name="authenticationManager"> <ref local="authenticationManager" /> </property> <property name="accessDecisionManager"> <ref local="httpRequestAccessDecisionManager" /> </property> <property name="objectDefinitionSource"> <value> <![CDATA[CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON \A/webservices/.*\Z=Admin]]> </value> </property> </bean>
Client Code
URL url = new URL("http://localhost:8080/pentaho/webservices/RequestDumper"); HttpClient httpClient = new HttpClient(); List authPrefs = new ArrayList(1); authPrefs.add(AuthPolicy.DIGEST); httpClient.getParams().setParameter(AuthPolicy.AUTH_SCHEME_PRIORITY, authPrefs); final String username = "joe"; final String password = "password"; Credentials credentials = new UsernamePasswordCredentials(username, password); AuthScope authScope = new AuthScope(AuthScope.ANY_HOST, AuthScope.ANY_PORT, AuthScope.ANY_REALM); httpClient.getState().setCredentials(authScope, credentials); PostMethod postMethod = new PostMethod(url.toExternalForm()); postMethod.setDoAuthentication(true); // send creds when challenged (when we receive response code 401) try { int result = httpClient.executeMethod(postMethod); // do something } catch (Exception e) { e.printStackTrace(); } finally { postMethod.releaseConnection(); }