Sandor Molnar created KNOX-3404:
-----------------------------------

             Summary:  Support optional principal normalization in the pac4j 
identity adapter
                 Key: KNOX-3404
                 URL: https://issues.apache.org/jira/browse/KNOX-3404
             Project: Apache Knox
          Issue Type: New Feature
          Components: Server
    Affects Versions: 2.1.0, 1.6.1, 1.6.0, 2.0.0, 1.5.0, 3.0.0
            Reporter: Sandor Molnar
            Assignee: Sandor Molnar
             Fix For: 3.1.0


h2. Summary

Add an optional, config-driven transformation of the authenticated principal in 
{{{}Pac4jIdentityAdapter{}}}, applied at the point the principal is finalized 
(after the id-attribute / NameID resolution). This lets operators normalize 
identities that are syntactically valid for SAML/pac4j but invalid for 
downstream consumers that impose stricter identifier rules - for example 
username values that must conform to Kubernetes label/name syntax.
h2. Background / Problem

When Knox federates authentication via the pac4j provider (e.g. a SAML client), 
the principal name is taken verbatim from either a configured SAML attribute or 
the IdP's NameID and used as-is, see in {{{}Pac4jIdentityAdapter.doFilter(){}}}:
{code:java}
if (idAttribute != null) {
  Object attribute = profile.getAttribute(idAttribute);
  if (attribute != null) id = attribute.toString();
}
if (id == null) {
  id = profile.getId();          // NameID fallback
}
PrimaryPrincipal pp = new PrimaryPrincipal(id);   // used verbatim
{code}
Whatever the IdP asserts becomes the Knox principal, and that principal is the 
single source of truth propagated to every downstream consumer - the 
{{actor-id}} request header emitted by {{ConfigurableDispatch}} 
({{{}getPrimaryPrincipalName{}}}), the {{actor-id}} header emitted by the 
KNOX-AUTH-SERVICE, the audit context, and the SSO JWT {{{}sub{}}}.

This breaks integration with downstream services that impose identifier 
constraints the IdP does not. Concretely: many IdPs assert the username in 
email/UPN form (e.g. {{{}[email protected]{}}}). When such a principal is 
propagated to a downstream service that uses it to construct a *Kubernetes 
namespace label value* (or a namespace name / DNS-1123 label), provisioning 
fails deterministically, because {{@}} is not a permitted character in 
Kubernetes label values (and email form also violates the stricter RFC-1123 
rules used for names/labels: lowercase alphanumerics plus {{{}-{}}}, no 
{{{}@{}}}, no uppercase).

The failure is a function purely of the identity the IdP asserts; Knox 
propagates it faithfully. Today there is no supported way to normalize this 
within Knox without depending on the IdP to be reconfigured.
h3. Why existing mechanisms do not solve it
 * *Identity-assertion providers (Regex/Default/etc.) do not help for this 
path.* When they map the principal, {{continueChainAsPrincipal}} keeps the 
original name as the {{PrimaryPrincipal}} and stores the mapped name only as an 
{{{}ImpersonatedPrincipal{}}}. The actor-id headers and audit/JWT use 
{{{}getPrimaryPrincipalName{}}}, so the _original_ (email) value is still 
emitted. Identity-assertion mapping affects {{{}doAs{}}}/impersonation 
semantics, not the propagated principal.
 * *{{pac4j.id_attribute}} only selects an attribute; it does not transform.* 
It requires the IdP to release a suitable, constraint-safe attribute for every 
user, and if that attribute is missing for a user the adapter silently falls 
back to the (email) NameID — turning a deterministic failure into an 
intermittent one. It is also unusable when the only value the IdP emits is 
email-shaped (including IdPs whose {{{}preferred_username{}}}/UPN is itself an 
email).

h2. Proposed change

Introduce an optional principal transformation in {{{}Pac4jIdentityAdapter{}}}, 
reusing the existing {{RegexTemplate}} from {{gateway-util-common}} (already a 
dependency of the pac4j module; the same engine used by the {{Regex}} 
identity-assertion provider).

New init parameters:
||Param||Description||
|{{pac4j.id_attribute.regex}}|Regex applied to the resolved principal. 
Optional.|
|{{pac4j.id_attribute.template}}|Output template (e.g. \{1}) using capture 
groups from the regex. Optional.|

 

The transform is applied *after* the id-attribute/NameID resolution and 
*before* {{{}new PrimaryPrincipal(id){}}}, so it normalizes the principal 
regardless of its source:
{code:java}
if (id == null) {
  id = profile.getId();
}
if (idTemplate != null && id != null) {   // idTemplate built in init() when 
both params are set
  id = idTemplate.apply(id);
}
PrimaryPrincipal pp = new PrimaryPrincipal(id);
{code}
 
Example configuration (strip domain from an email-form principal, no-op for 
plain usernames):
{code:xml}
<param>
  <name>pac4j.id_attribute.regex</name>
  <value>^([^@]+)(@.*)?$</value>
</param>
<param>
  <name>pac4j.id_attribute.template</name>
  <value>{1}</value>
</param>
{code}
{{[email protected]}} -> {{{}user{}}}; {{user}} -> {{user}} (unchanged).
 
h2. Why this is the correct location
 * *Single choke point:* all pac4j logins funnel through this code before a 
{{PrimaryPrincipal}} is created, so normalization is uniform whether the id 
came from an attribute or the NameID fallback (closing the silent-fallback gap).
 * *Downstream consistency:* because every consumer reads the primary 
principal, one transformation here makes the actor-id headers, audit context, 
and SSO JWT {{sub}} all consistent automatically.
 * *IdP-independent:* it transforms the value Knox received rather than 
depending on IdP-side reconfiguration, so it works for externally-managed IdPs 
where the operator cannot control NameID format or attribute release.
 * *Safe to enable broadly:* with a regex that is the identity function for 
values that do not need changing (e.g. anything without {{{}@{}}}), principals 
that already conform are left untouched, so enabling it does not disturb 
existing clients that rely on the current value.

h2. Backward compatibility
 * Fully backward compatible. When neither {{pac4j.id_attribute.regex}} nor 
{{pac4j.id_attribute.template}} is set, behavior is unchanged (no 
transformation).
 * No change to existing {{pac4j.id_attribute}} semantics.

h2. Constraints / edge cases (document in user docs)
 * *Uniqueness / collisions:* a transform that drops the domain can collapse 
distinct identities ({{{}[email protected]{}}} and {{[email protected]}} -> {{{}user{}}}). 
Where multiple domains are possible, operators should choose a transform that 
preserves uniqueness (e.g. retain the domain, mapping {{@}} to a permitted 
separator). This is left to operator configuration.
 * *{{RegexTemplate}} behavior:* on no-match, {{RegexTemplate.apply}} returns 
the output template literally; the configured regex must therefore match all 
expected inputs. Note this in documentation and cover it with tests.
 * *Target syntax varies:* downstream identifier rules differ (Kubernetes label 
_value_ permits {{{}-{}}}, {{_}}, {{{}.{}}}; a namespace _name{_} / DNS-1123 
label is stricter — lowercase, no {{{}_{}}}/{{{}.{}}}). {{RegexTemplate}} 
performs structural extraction only (no case-folding). The regex/template must 
be chosen to satisfy the specific downstream constraint; for case-folding 
needs, {{SwitchCase}} identity assertion or a stricter transform may be layered.

h2. Acceptance criteria
 * New optional params {{pac4j.id_attribute.regex}} and 
{{pac4j.id_attribute.template}} are read in {{Pac4jIdentityAdapter.init()}} 
and, when both present, build a {{{}RegexTemplate{}}}.
 * The transform is applied to the resolved id (from either the configured 
attribute or the NameID fallback) before the {{PrimaryPrincipal}} is created.
 * When the params are absent, the principal is unchanged (regression-safe).
 * The resulting principal is what appears in the actor-id header, audit log, 
and SSO token.
 * User documentation for the pac4j provider describes the new params, an 
email-> username example, and the collision/no-match caveats.

h2. Testing
 * Unit tests in {{Pac4jIdentityAdapter}} covering: transform applied to an 
attribute-sourced id; transform applied to a NameID-fallback id; email-form 
input normalized; non-matching/plain-username input left unchanged (identity 
regex); params absent -> verbatim behavior preserved.
 * Reuse/extend existing {{RegexTemplate}} tests as needed for the chosen 
patterns.



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to