Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
[backend] Allow to quote path component in double quotes (#14781)
  • Loading branch information
xfournet committed Mar 4, 2026
commit 43bf146a29c9dd84adf8ccab9f396f801f0663e0
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export const ENTITY_TYPE_AUTHENTICATION_PROVIDER = 'AuthenticationProvider';
// Mapping configuration
//
// All _expr fields use dot-separated notation (e.g. 'user_info.email', 'tokens.access_token.groups').
// Resolution splits on '.' and traverses the path: resolvePath(root, expr.split('.'))
// Path components that contain dots can be quoted with double quotes: "http://example.com/claims/email".
//
// OIDC: context = { tokens: fn(name), user_info: fn() } — paths like 'user_info.email' or 'tokens.id_token.sub'
// SAML: context = profile.attributes ?? profile — paths like 'email' (flat) or 'org.list' (nested)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,25 @@ export const resolvePath = (path: string[]) => async (obj: unknown): Promise<any
return resolvePath(remaining)(resolvedValue);
};

export const resolveDotPath = (path: string) => resolvePath(path.split('.'));
/**
* Parses a dot-separated path string into segments.
* Path components that contain dots (e.g. SAML HTTP claim URIs) can be quoted with double quotes.
* Inside quoted segments, use "" to represent a literal double quote.
* Example: user_info."http://example.com/claims/email" → ['user_info', 'http://example.com/claims/email']
*/
export const parseDotPath = (path: string): string[] => {
// Unquoted [^."]+ or quoted "((?:[^"]|"")*)", then . or end. "" inside quotes = literal ".
const re = /(?:([^."]+)|"((?:[^"]|"")*)")(?:\.|$)/g;
const segments: string[] = [];
let m: RegExpExecArray | null;
while ((m = re.exec(path)) !== null) {
const segment = m[1] !== undefined ? m[1] : m[2].replace(/""/g, '"');
segments.push(segment);
}
return segments;
};

export const resolveDotPath = (path: string) => resolvePath(parseDotPath(path));

type ResolveExprFunction = (obj: unknown) => undefined | string | string[] | Promise<undefined | string | string[]>;
type CreateResolveExprFunction = (expr: string) => ResolveExprFunction;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,55 @@
import { describe, expect, it } from 'vitest';
import { resolvePath } from '../../../../src/modules/authenticationProvider/mappings-utils';
import { parseDotPath, resolvePath, resolveDotPath } from '../../../../src/modules/authenticationProvider/mappings-utils';

describe('mappings-utils', () => {
describe('parseDotPath', () => {
it('should split simple dot-separated path', () => {
expect(parseDotPath('a.b.c')).toEqual(['a', 'b', 'c']);
});

it('should treat quoted segment as single component (SAML URI)', () => {
expect(parseDotPath('user_info."http://example.com/claims/email"')).toEqual(['user_info', 'http://example.com/claims/email']);
});

it('should support multiple quoted segments', () => {
expect(parseDotPath('"http://a".b."http://c"')).toEqual(['http://a', 'b', 'http://c']);
});

it('should allow escaped double quote inside quoted segment (double double quote)', () => {
expect(parseDotPath('"say ""hi"""')).toEqual(['say "hi"']);
});

it('should return single segment when no dots', () => {
expect(parseDotPath('mail')).toEqual(['mail']);
});

it('should handle empty path', () => {
expect(parseDotPath('')).toEqual([]);
});

it('should handle trailing dot (empty segment not pushed)', () => {
expect(parseDotPath('a.')).toEqual(['a']);
});
});

describe('resolveDotPath', () => {
it('should resolve path with quoted segment (SAML-style URI key)', async () => {
const obj = {
attributes: {
'http://example.com/claims/email': 'user@example.com',
},
};
const result = await resolveDotPath('attributes."http://example.com/claims/email"')(obj);
expect(result).toBe('user@example.com');
});

it('should resolve plain dot path as before (backward compatible)', async () => {
const obj = { user_info: { email: 'a@b.com' } };
const result = await resolveDotPath('user_info.email')(obj);
expect(result).toBe('a@b.com');
});
});

describe('resolvePath', () => {
it('should resolve simple property', async () => {
const obj = { foo: 'bar' };
Expand Down
Loading