File(s): conf/intercept/impersonate-intercept-config.xml (V4.0), conf/idp.properties (V4.1+), views/intercept/impersonate.vm, conf/access-control.xml
Format: Native Spring, Properties (V4.1+), Velocity

Overview

The "impersonate" interceptor flow is an example of how to use an interceptor to implement a testing capability that allows a user to represent themselves as a different user to a downstream service, subject to appropriate authorization. It does not affect the user's overall session and has no persistent effect on access to other services within the same session; it is a per-request feature that changes the set of attributes resolved by re-running the attribute resolver as though the user had a different identity.

All interceptors are enabled or disabled on a per-relying-party basis using properties in the profile bean(s) you want to enable the flow for. See the ProfileInterceptConfiguration topic for an example.

Enabling Module (V4.1+)

For V4.1+, configuring and using this feature requires that you first enable the "idp.intercept.Impersonate" module if it isn't already enabled. Systems upgraded from older releases generally come pre-enabled due to the prior state of the configuration tree.

(Windows)
C:\opt\shibboleth-idp> bin\module.bat -t idp.intercept.Impersonate || bin\module.bat -e idp.intercept.Impersonate
 
(Other)
$ bin/module.sh -t idp.intercept.Impersonate || bin/module.sh -e idp.intercept.Impersonate

General Configuration

The primary configuration involved with this flow is to define AccessControl policies to control who can impersonate whom. And of course, one can customize the view template displayed.

The beans named shibboleth.impersonate.GeneralPolicy and shibboleth.impersonate.SpecificPolicy in conf/intercept/impersonate-intercept-config.xml must be defined by you with the named AccessControl policies you want to apply.

The properties named idp.impersonate.generalPolicy and idp.impersonate.specificPolicy control the named AccessControl policies you want to apply. They default to "GeneralImpersonationPolicy" and "SpecificImpersonationPolicy" respectively.

If the legacy file conf/intercept/impersonate-intercept-config.xml remains present, the beans in that file supersede the property definition(s) but you may remove the file if the default values or properties are used instead.

The "general" policy runs first up front and determines whether to actually offer the impersonation option. This is logically equivalent to attaching an activation condition to the flow itself, but the AccessControl service is reloadable, so using a policy is more flexible.

The "specific" policy runs after an account name is selected to decide whether to allow it.

Since retrieving a policy by name that doesn't exist results in denial of access, the system is safe out of the box. Enabling the flow won't actually cause it to run due to the lack of a matching policy defined to allow access to it.

The view to display is contained in views/intercept/impersonate.vm, and it provides an example allowing the user to enter the account to impersonate using a form field, or to bypass the impersonation and just continue normally.

Flow Behavior

If the general policy grants access, the view is rendered. Assuming a username is entered and the impersonation is attempted, the flow runs the specific policy. This policy is evaluated with its "resource" input set to the intended account name to impersonate, allowing the policy to react accordingly.

If the policy denies access, the failed attempt is logged and an event named "ImpersonationViolation" terminates the entire request. This is an event you can configure whatever error handling you prefer in response to (see ErrorHandlingConfiguration), but for new installs it's defined to be a "local error". Upgraded systems will need to add that event to the map of local events (you can compare your copy of conf/errors.xml to the one in the dist directory).

If the policy allows access, the flow will:

Attribute Resolver Considerations

For most configurations, the only user-specific element of the resolution process will be derived from the principalName property of the AttributeResolutionContext provided to the resolver, which will be set by this flow to the impersonated name instead of the original one.

However, you should thoroughly examine and test your configuration to be sure that you're getting the output you expect. In particular, if you have behavior (likely via scripts or the advanced SubjectDerivedAttributeAttributeDefinition) that depends on content internal to the user's authentication state, this will not be effectively overridden by this flow.

To facilitate attaching conditions or adding script logic that can be made dependent on whether impersonation is happening, a new property has been added to the AttributeResolutionContext, the resolutionLabel, and it will be set to "intercept/impersonate" (the flow ID) when this flow is doing the resolution (it will be empty in the traditional case).

As a general matter, unless all of your attributes come from a persistent store you can re-query, this feature is not likely to work, or be safe to use.

Reference

The following beans are defined in conf/intercept/impersonate-intercept-config.xml:

Bean ID

Type

Description

shibboleth.impersonate.GeneralPolicy

String

Named AccessControl policy to run to determine whether to run this flow 

shibboleth.impersonate.SpecificPolicy

String

Named AccessControl policy to run to determine whether to allow the requested impersonation

The following properties in conf/idp.properties may be used to override the default policy names used:

Name

Default

Description

idp.impersonate.generalPolicy

GeneralImpersonationPolicy

Named AccessControl policy to run to determine whether to run this flow 

idp.impersonate.specificPolicy

SpecificImpersonationPolicy

Named AccessControl policy to run to determine whether to allow the requested impersonation

Example

Aside from the UI, all of the flow's configuration is actually just defining policies, either in conf/access-control.xml or an included file. In practice, a "real world" implementation of such policies would likely rely on some kind of directory or database of rules controlling which users can impersonate which users to which services, perhaps through group memberships resolved during initial attribute resolution.

For illustrative purposes, consider a rule that the flow is authorized only to users possessing a particular entitlement value, and then are allowed to impersonate any users named by a second custom attribute to services named by a third. This fully devolves the decision making to data that could be supplied from any directory or database, instead of internal rules hardcoded into the configuration.

The "general" policy is a simple test of an entitlement attribute for a specific value that is intended to mean "allow this logged-in user to at least attempt impersonation", and uses the SimpleAttributePredicate Java class. This class is often used to implement authorization rules in the context-check interceptor.

The "specific" policy could be implemented by a somewhat-involved script, but the example below relies on an advanced Java class that checks the values of a resolved IdPAttribute against a dynamically computed candidate value (or values) produced by functions, which can themselves be scripts or expressions. This DynamicAttributePredicate class relies on an injected map of attribute names to functions, instead of attribute names to strings. The mapped function objects return the data values that have to be found in the named attribute at runtime to be a valid match. It's indirect instead of static/direct.

The example uses this trick to derive the values to look for from the actual impersonation attempt. The ID of the service and the ID of the impersonated user come from functions and those functions are what the example is putting in the map that the Java class calls.

Instead of a rule saying "can impersonate jdoe to service xyz", which would be static and hard to manage, the rule is saying "allow the impersonation of jdoe to xyz if jdoe is one of the values of the impersonatableUsernames IdPAttribute and xyz is one of the values of the impersonatableServices IdPAttribute".

Once you understand the idea, it's not a lot of configuration to maintain and supports a plausible real-world approach to authorizing the impersonation feature.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:util="http://www.springframework.org/schema/util"
       xmlns:p="http://www.springframework.org/schema/p"
       xmlns:c="http://www.springframework.org/schema/c"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
                           http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"
                           
       default-init-method="initialize"
       default-destroy-method="destroy">

    <util:map id="shibboleth.AccessControlPolicies">
    
		<!-- Limits who can impersonate based on entitlement. -->
        <entry key="GeneralImpersonationPolicy">
            <bean parent="shibboleth.PredicateAccessControl">
                <constructor-arg>
                    <bean class="net.shibboleth.idp.profile.logic.SimpleAttributePredicate">
                        <property name="attributeValueMap">
                            <map>
                                <entry key="eduPersonEntitlement">
                                    <list>
                                        <value>https://example.org/entitlement/impersonation</value>
                                    </list>
                                </entry>
                            </map>
                        </property>
                    </bean>
                </constructor-arg>
            </bean>
        </entry>

		<!-- Controls the impersonation scenarios to allow. -->
        <entry key="SpecificImpersonationPolicy">
            <bean parent="shibboleth.PredicateAccessControl">
                <constructor-arg>
                    <bean parent="shibboleth.Conditions.AND">
                        <constructor-arg>
                            <bean class="net.shibboleth.idp.profile.logic.DynamicAttributePredicate">
                                <property name="attributeFunctionMap">
                                    <map>
                                        <entry key="impersonatableUsernames">
                                            <list>
                                                <bean parent="shibboleth.ContextFunctions.Expression"
                                                    c:expression="#input.getSubcontext(T(org.opensaml.profile.context.AccessControlContext)).getResource()" />
                                            </list>
                                        </entry>
                                    </map>
                                </property>
                            </bean>
                        </constructor-arg>
                        <constructor-arg>
                            <bean class="net.shibboleth.idp.profile.logic.DynamicAttributePredicate">
                                <property name="attributeFunctionMap">
                                    <map>
                                        <entry key="impersonatableServices">
                                            <list>
                                                <bean parent="shibboleth.RelyingPartyIdLookup.Simple" />
                                            </list>
                                        </entry>
                                    </map>
                                </property>
                            </bean>
                        </constructor-arg>
                    </bean>
                </constructor-arg>
            </bean>
        </entry>

    </util:map>

</beans>