Impersonation Managed By Grouper

The examples in this page reflect certain approaches required by IdP V5. They are not entirely compatible with earlier versions, though the differences are fairly minor relative the point of the example.

Overview

This is a companion example to the more more general https://shibboleth.atlassian.net/wiki/spaces/KB/pages/1432060485. It demonstrates an approach to leveraging the IdP’s impersonation feature, while using a particular design pattern in Grouper to model and manage the authorization decisions that are required by that feature. It is not by any means the only way to manage impersonation, nor the only way to use Grouper to do that, as the feature is extremely generic and merely requires that a pair of access control policies exist to supply answers to the basic questions “can this user impersonate anybody to this service?” and “can this user impersonate this subject to this service?”.

In this example, these questions are answered via a single Grouper WS query that is parameterized by two inputs: the service involved and the original user identity, after the c14n process normalizes the username. The attribute resolver is programmed to query Grouper for that user’s group memberships in a particular tree specific to the service. If no such tree exists, or no memberships exist for the user in that tree, impersonation is disallowed. This is of course the typicall situation.

If at least one membership exists, then the group stem name(s) are processed to obtain the final portion of the stem name, which contains the identity of a subject that the user is permitted to impersonate. In doing so, this allows the UI to actually provide a drop down list of those names to pick from, and the system will perform a final validation by ensuring that the value selected is in fact one of the authorized groups. (This prevents an attack against the UI if a user were to manipulate their submission to inject a name of their own choosing.)

This model has some advantages:

  • Efficiency – only one group query is performed

  • Delegation – control over who can impersonate what users to which services can be delegated out via Grouper’s usual security features

  • Fails Safely – a failed query results in no group memberships, ensuring no impersonation is allowed

  • Usability – users can select from a predefined set of impersonatable identities instead of requiring data entry

  • Loose Coupling – Grouper does not treat the impersonated names as “subjects”, so they need not actually be present in any Grouper subject source

  • Low IdP Operational Overhead – the impersonation feature can be safely enabled globally, so changes in Grouper alone will be sufficient to add new impersonation permissions without modifying IdP configuration

With respect to the latter point, as with the original example, metadata tagging is used to actually trigger to Grouper query. Absent such a tag, no query is performed, and no impersonation is possible. The only added overhead is a relatively fast attempt to run the flow, evaluate an attribute-based access control check, determine that no impersonation values are available, and skip the flow.

Grouper Structure

The folder layout in Grouper that matches these examples looks like this:

  • OSU

    • WebLoginService

      • <SHA-1 hashed entityID of SP>

        • impersonate

Within that folder, each username to be impersonated is created as a group. The group’s member’s then contain the subjects (in the Grouper sense) that will be allowed to impersonate the group’s name to the service within which the whole tree is contained. The requirement to tie this together is that the authenticated user within the IdP must be known to Grouper, and the IdP must be able to obtain a piece of data about the user with which to query Grouper. In our case, we use our IDM ID as the unique subject ID in Grouper, so the IdP needs to obtain that data element to query with.

Tagging

The metadata extension attribute used in this example is the following:

<saml:Attribute NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri" Name="urn:mace:osu.edu:shibboleth:attribute-release"> <saml:AttributeValue>grouperImpersonation</saml:AttributeValue> </saml:Attribute>

This can be embedded directly in an <mdattr:EntityAttributes> extension, or added at runtime to an external metadata source using the EntityAttributesFilter feature.

This tag will be used in other parts of the example to control activation of components. Obviously it’s a custom Attribute I invented for my own use, but as with all of my work, URIs are always used to ensure uniqueness and prevent any possible conflicts with other settings. String naming should never be used in any SAML context under any circumstances.

Attribute Resolver

Most of the complex bits are in the resolver obviously, to pull in Grouper data and format it into impersonatable usernames. Most of this is accomplished with scripts of various sorts.

A note of caution: these examples are written using the pre-Java 8 Rhino scripting engine because that's what I've stuck with in my deployment as a personal preference (you'll see the language attribute used explicitly to highlight this). Using the examples with Nashorn would require some re-writes, but nothing dramatic.

Supporting Beans

Often, advanced use cases require some native Spring wiring for certain kinds of objects, and this is definitely one of those cases. I load beans needed for the resolver in a separate Spring file added as an additional resource to conf/services.xml.

There are a variety of beans here, divided into two categories:

  • Some fairly simple beans needed to control or customize the behavior of the HTTPConnector itself.

  • More complex beans that define a custom-purpose HttpClient for the web service calls, including specialized security configuration to support pre-emptive HTTP Basic Authentication to authenticate via a service account to Grouper.

The custom client provides for better handling of timeout behavior, and the security beans provide appropriate TLS validation of the server, and avoid extra round trips by providing the HTTP credentials in every call instead of waiting for a 403 challenge from the server.

A description of some of the beans follows the example.

<bean id="pathEscaper" class="com.google.common.net.UrlEscapers" factory-method="urlPathSegmentEscaper" /> <bean id="osu.StringDigester" class="net.shibboleth.shared.codec.StringDigester" c:algorithm="SHA1" c:format="HEX_LOWER" /> <!-- GMS impersonation lookup. --> <bean id="ImpersonationCondition" parent="shibboleth.Conditions.AND"> <constructor-arg> <list> <bean parent="shibboleth.Conditions.Expression" c:expression="!#input.getSubcontext(T(net.shibboleth.idp.attribute.resolver.context.AttributeResolutionContext)).getResolutionLabel().equals('intercept/impersonate')" /> <bean parent="shibboleth.Conditions.EntityDescriptor"> <constructor-arg name="pred"> <bean parent="shibboleth.Conditions.MappedEntityAttributes"> <constructor-arg> <list> <bean parent="shibboleth.TagCandidate" c:name="urn:mace:osu.edu:shibboleth:attribute-release" p:values="grouperImpersonation" /> </list> </constructor-arg> </bean> </constructor-arg> </bean> </list> </constructor-arg> </bean> <bean id="grouperHttpClient" parent="shibboleth.HttpClientFactory" lazy-init="true" p:maxConnectionsPerRoute="20" p:maxConnectionsTotal="20" p:connectionTimeout="PT2S" p:connectionRequestTimeout="PT2S" p:socketTimeout="PT5S" p:tLSSocketFactory-ref="shibboleth.SecurityEnhancedTLSSocketFactory" /> <bean id="grouperHttpSecurity" lazy-init="true" class="org.opensaml.security.httpclient.HttpClientSecurityParameters" p:preemptiveBasicAuthMap-ref="grouperAuthMap"> <property name="tLSTrustEngine"> <bean parent="shibboleth.StaticPKIXTrustEngine" p:checkNames="true" p:trustedNames="*.service.osu.edu" p:verifyDepth="3"> <property name="certificates"> <list> <value>%{idp.home}/credentials/usertrust.pem</value> </list> </property> </bean> </property> </bean> <util:map id="grouperAuthMap"> <entry> <key> <bean parent="shibboleth.HttpHost" p:scheme="https" p:hostname="group-management-ws.service.osu.edu" p:port="443" /> </key> <bean parent="shibboleth.BasicAuthCredentials" p:username="%{idp.grouper-ws.username}" p:password="%{idp.grouper-ws.password}" /> </entry> </util:map> <util:map id="osu.GroupsCustomObjects"> <entry key="digester" value-ref="osu.StringDigester" /> <entry key="servletRequestSupplier" value-ref="shibboleth.HttpServletRequestSupplier" /> </util:map>

I'm not going to cover all of this because you should be able to look up the Javadocs for most of it yourself, but the HTTP client security is fairly involved and needs explanation. It's easier to explain from the bottom up.

The bottom of the stack here is the "grouperAuthMap", which is a way of priming the client with information about specific web servers that require authentication so that it doesn't wait to be prompted by the server and cause extra round trips. This is only safe to do if you have good control over the TLS validation process to make sure connections are only made to trusted servers, which is handled later.

The "grouperHttpSecurity" bean is the Shibboleth-defined object that encapsulates the securing of the HTTP client. Some of the aspects of this class are discussed on the HttpClientConfiguration page. The map mentioned above is injected into one property. A trust engine is also injected that handles verification of the server's certificate. The CA certificate is defined with a property, and the depth of the chain is extended to allow for intermediate CAs. Because the built-in code does not understand wildcard certificates (which are a bad thing in general, but...) the name of the certificate has to be manually added to the configuration to allow it to be matched.

Note that the "grouperHttpClient" and "grouperHttpSecurity" beans are not tied together here. In general, client objects are independent of security, and the security features are applied separately to the object using the client (in this case the eventual HTTPConnector). This allows a single client to be used against different servers if appropriate.

Lastly, note that the activation condition there is not strictly “check for the metadata tag” but “check for the tag AND that the resolution label is NOT “intercept/impersonate” (there’s a bang symbol in front of that check). That bypasses the Grouper lookup after impersonation is authorized and in use, when resolving attributes for the impersonated identity since that would be silly and redundant.

Data Connector

The most interesting part of this example is the HTTPConnector itself, and how it forms the request and handles the response. For convenience the script that parses the JSON response is in a separate file, so the connector itself is shorter:

<DataConnector id="grouperImpersonation" xsi:type="HTTP" activationConditionRef="ImpersonationCondition" httpClientRef="grouperHttpClient" httpClientSecurityParametersRef="grouperHttpSecurity" acceptTypes="application/json"> <InputAttributeDefinition ref="IDMUID" /> <URLTemplate customObjectRef="osu.StringDigester"> <![CDATA[ <https://group-management-ws.service.osu.edu/gms-ws/servicesRest/v2.6.000/subjects/#if($IDMUID> && $IDMUID.size() == 1)$IDMUID.get(0)#end/groups?wsLiteObjectType=WsRestGetGroupsLiteRequest&stemName=OSU%3AWebLoginService%3A$custom.apply($resolutionContext.attributeRecipientID)%3Aimpersonate&stemScope=ALL_IN_SUBTREE ]]> </URLTemplate> <ResponseMapping language="rhino" customObjectRef="osu.GroupsCustomObjects"> <ScriptFile>%{idp.home}/conf/js/grouperImpersonation.js</ScriptFile> </ResponseMapping> </DataConnector>

The main element contains a lot of the references to the objects defined in the other file, mentioned earlier, like the activation condition, the HTTP client bean, and the security bean. The other interesting property is one that causes the client to advertise it supports JSON using a MIME type that Grouper understands. Without that, the web service returns XML instead.

The bottom part just defines the script file to run to process the response and is covered later.

The <URLTemplate> element is the interesting bit but it's mostly obvious except for the encoding of the entityID and if you weren't aware that, as a Velocity template, these kinds of query templates can do conditionals, though it's a bit unreadable. The conditional just error-guards a missing input attribute. The stem for the search here is the part that standardizes the interaction. We hash the entityID using the custom bean defined earlier, so the stem format in Grouper is "OSU:WebLoginService:<hashed>:impersonate". The hashing avoids encoding issues in Grouper, and we use display names on that side to hide them from the application owners.

The response parsing script is below, and it's a "real-time" handler, meaning it consumes the data exactly once, so it's suitable for streaming responses without a content length. The helper method enforces a limit on size as it consumes the data.

Most of this is just parsing the Grouper result, but Grouper has a critical, unfixed bug that causes a request for a non-existent stem to return every group membership the requesting account can see. To prevent this, the code checks that the stem prefix is as expected, and if not, that the data should be cleared and nothing returned.

Apart from that, the username suffix of each group membership is stripped off and populated as a value of an array list. Provided at least one exists, the array list is then converted into values of a resolver attribute named “impersonatableUsernames“.

Impersonation Policies

With all this machinery in place, the connection back to the impersonation process involves definiing the to access control policies that the feature relies on.

The “general” policy which controls whether the impersonation flow runs at all is simply a check for whether the “impersonatableUsernames” IdPAttribute computed via the script above based on Grouper data has any values at all. That is, if it has a value, then at least one impersonatable identity was authorized for the user, so they have the option to select one to use or proceed as themselves.

The “specific” policy which controls whether the specific identity chosen is acceptable or not is then evaluated against the values present in the IdPAttribute indirectly using a special condition defined for this use case. In English, what this does is define a function to run to obtain the value to check for in the values of the IdPAttribute. The function in this case is a Spring Expression that extracts the “resource” the access control policy is being run against, which for impersonation checking is the actual identity to impersonate.