Skip to content

Next major: add preserveImpersonation option to setIdentity() / clearIdentity() #797

@dereuromark

Description

@dereuromark

Background

PR #788 added AuthenticationComponent::replaceIdentity() for the common request-only identity refresh case (e.g. attaching eager-loaded associations in beforeFilter() without ending an active impersonation).

An earlier iteration of that PR also expanded two existing signatures:

  • AuthenticationComponent::setIdentity(ArrayAccess|array $identity, bool $preserveImpersonation = false)
  • AuthenticationService::clearIdentity(ServerRequestInterface $request, ResponseInterface $response, bool $stopImpersonation = true)

Those were reverted before merge to keep the public API surface unchanged on the 4.x minor. The signature growth belongs in the next major, where adding the flag to PersistenceInterface::clearIdentity() itself is clean.

Proposal for the next major

Add a first-class "persist a refreshed identity while keeping impersonation alive" path. Unlike replaceIdentity() (request attribute only, lost on the next request), this survives into subsequent requests.

1. Extend the interface

// PersistenceInterface
public function clearIdentity(
    ServerRequestInterface $request,
    ResponseInterface $response,
    bool $stopImpersonation = true,
): array;

2. Honor the flag in AuthenticationService::clearIdentity()

public function clearIdentity(
    ServerRequestInterface $request,
    ResponseInterface $response,
    bool $stopImpersonation = true,
): array {
    foreach ($this->authenticators() as $authenticator) {
        if ($authenticator instanceof PersistenceInterface) {
            if (
                $stopImpersonation
                && $authenticator instanceof ImpersonationInterface
                && $authenticator->isImpersonating($request)
            ) {
                $stopImpersonationResult = $authenticator->stopImpersonating($request, $response);
                ['request' => $request, 'response' => $response] = $stopImpersonationResult;
            }
            $result = $authenticator->clearIdentity($request, $response);
            ['request' => $request, 'response' => $response] = $result;
        }
    }
    if ($stopImpersonation) {
        $this->_successfulAuthenticator = null;
    }

    return [
        'request' => $request->withoutAttribute($this->getConfig('identityAttribute')),
        'response' => $response,
    ];
}

3. Add the opt-in to the component

public function setIdentity(ArrayAccess|array $identity, bool $preserveImpersonation = false)
{
    $controller = $this->getController();
    $service = $this->getAuthenticationService();

    $service->clearIdentity(
        $controller->getRequest(),
        $controller->getResponse(),
        stopImpersonation: !$preserveImpersonation,
    );

    /** @var array{request: \Cake\Http\ServerRequest, response: \Cake\Http\Response} $result */
    $result = $service->persistIdentity(
        $controller->getRequest(),
        $controller->getResponse(),
        $identity,
    );

    $controller->setRequest($result['request']);
    $controller->setResponse($result['response']);

    return $this;
}

Usage

// Refresh the impersonated user and keep it across requests, without
// reverting to the impersonator:
$this->Authentication->setIdentity($reloaded, preserveImpersonation: true);

Note this also skips the session rotation that the default setIdentity() flow performs - it is a refresh, not a privilege transition, so it must not be used for login or role changes. That caveat should be documented.

Notes

  • Because clearIdentity() is on PersistenceInterface, adding the parameter there is a hard BC break and is why this is deferred to the major.
  • Tests for both the preserveImpersonation: true persist case and the default-ends-impersonation case existed in the PR branch history and can be lifted from there.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions