Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
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
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ export function registerSearchRoute(
enabled: false,
reason: 'This route is opted out from authorization',
},
authc: {
enabled: 'minimal',
reason: 'This route is optimized for performant retrieval of data from Elasticsearch.',
},
},
})
.addVersion(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ describe('Search service', () => {

expect(mockSessionClient.trackId).toBeCalledTimes(1);

expect(mockSessionClient.trackId.mock.calls[0]).toEqual(['my_id', options]);
expect(mockSessionClient.trackId.mock.calls[0]).toEqual(['my_id', options, true]);
});

it('does not call `trackId` if search is already tracked', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -374,7 +374,12 @@ export class SearchService {
return request;
} else {
try {
const id = await deps.searchSessionsClient.getId(request, options);
const id = await deps.searchSessionsClient.getId(
request,
options,
// The search route uses minimal auth for performance, which doesn't include realm information.
true
);
this.logger.debug(`Found search session id for request ${id}`);
return {
...request,
Expand Down Expand Up @@ -417,7 +422,14 @@ export class SearchService {
isStored: true,
});
} else {
return from(deps.searchSessionsClient.trackId(response.id, options)).pipe(
return from(
deps.searchSessionsClient.trackId(
response.id,
options,
// The search route uses minimal auth for performance, which doesn't include realm information.
true
)
).pipe(
tap(() => {
isInternalSearchStored = true;
}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,24 @@ const MAX_UPDATE_RETRIES = 3;

const flushPromises = () => new Promise((resolve) => setImmediate(resolve));

const mockMinimalAuthUser = new Proxy(
{
username: 'my_username',
enabled: true,
authentication_provider: { type: 'basic', name: 'basic1' },
} as any,
{
get(target, prop) {
if (prop === 'authentication_realm') {
throw new Error(
`Property "${String(prop)}" is not available for minimally authenticated users.`
);
}
return Reflect.get(target, prop);
},
}
);

describe('SearchSessionService', () => {
let savedObjectsClient: jest.Mocked<SavedObjectsClientContract>;
let asCurrentUserElasticsearchClient: ElasticsearchClientMock;
Expand Down Expand Up @@ -838,6 +856,64 @@ describe('SearchSessionService', () => {
},
});
});

describe('minimal auth user', () => {
it('throws by default', async () => {
const requestHash = faker.string.alpha(64);
const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0';

const mockUpdateSavedObject = {
...mockSavedObject,
attributes: {},
};
savedObjectsClient.update.mockResolvedValue(mockUpdateSavedObject);
savedObjectsClient.get.mockResolvedValue(mockSavedObject);

await expect(
service.trackId({ savedObjectsClient }, mockMinimalAuthUser, searchId, {
sessionId,
strategy: MOCK_STRATEGY,
requestHash,
})
).rejects.toThrow();

expect(savedObjectsClient.update).not.toHaveBeenCalled();
expect(mockLogger.debug).not.toHaveBeenCalledWith(
expect.stringContaining('Skipping realm check')
);
});

it('works when auth realm check is skipped', async () => {
const requestHash = faker.string.alpha(64);
const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0';

const mockUpdateSavedObject = {
...mockSavedObject,
attributes: {},
};
savedObjectsClient.update.mockResolvedValue(mockUpdateSavedObject);
savedObjectsClient.get.mockResolvedValue(mockSavedObject);

await expect(
service.trackId(
{ savedObjectsClient },
mockMinimalAuthUser,
searchId,
{
sessionId,
strategy: MOCK_STRATEGY,
requestHash,
},
true // skipRealmCheck
)
).resolves.not.toThrow();

expect(savedObjectsClient.update).toHaveBeenCalled();
expect(mockLogger.debug).toHaveBeenCalledWith(
expect.stringContaining('Skipping realm check')
);
});
});
});

describe('getId', () => {
Expand Down Expand Up @@ -902,6 +978,84 @@ describe('SearchSessionService', () => {

expect(id).toBe(searchId);
});

describe('minimal auth user', () => {
it('throws by default', async () => {
const searchRequest = { params: {} };
const requestHash = faker.string.alpha(64);
const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0';
const mockSession = {
...mockSavedObject,
attributes: {
...mockSavedObject.attributes,
idMapping: {
[requestHash]: {
id: searchId,
},
},
},
};
savedObjectsClient.get.mockResolvedValue(mockSession);

const getIdPromise = service.getId(
{ savedObjectsClient },
mockMinimalAuthUser,
searchRequest,
{
sessionId,
isStored: true,
isRestore: true,
requestHash,
}
);

await expect(getIdPromise).rejects.toThrow();

expect(savedObjectsClient.update).not.toHaveBeenCalled();
expect(mockLogger.debug).not.toHaveBeenCalledWith(
expect.stringContaining('Skipping realm check')
);
});

it('works with minimal auth user (no authentication_realm access)', async () => {
const searchRequest = { params: {} };
const requestHash = faker.string.alpha(64);
const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0';
const mockSession = {
...mockSavedObject,
attributes: {
...mockSavedObject.attributes,
idMapping: {
[requestHash]: {
id: searchId,
},
},
},
};
savedObjectsClient.get.mockResolvedValue(mockSession);

const getIdPromise = service.getId(
{ savedObjectsClient },
mockMinimalAuthUser,
searchRequest,
{
sessionId,
isStored: true,
isRestore: true,
requestHash,
},
true
);

await expect(getIdPromise).resolves.not.toThrow();

const id = await getIdPromise;
expect(id).toBe(searchId);
expect(mockLogger.debug).toHaveBeenCalledWith(
expect.stringContaining('Skipping realm check')
);
});
});
});

describe('getSearchIdMapping', () => {
Expand Down
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ interface TrackIdQueueEntry {

searchInfo: SearchSessionRequestInfo;
requestHash: string;
skipRealmCheck: boolean;
}

export class SearchSessionService implements ISearchSessionService {
Expand Down Expand Up @@ -163,14 +164,16 @@ export class SearchSessionService implements ISearchSessionService {
public get = async (
{ savedObjectsClient }: SearchSessionDependencies,
user: AuthenticatedUser | null,
sessionId: string
sessionId: string,
skipRealmCheck: boolean = false
) => {
this.logger.debug(`get | ${sessionId}`);
const session = await savedObjectsClient.get<SearchSessionSavedObjectAttributes>(
SEARCH_SESSION_TYPE,
sessionId
);
this.throwOnUserConflict(user, session);

this.throwOnUserConflict(user, session, skipRealmCheck);

return session;
};
Expand Down Expand Up @@ -226,11 +229,12 @@ export class SearchSessionService implements ISearchSessionService {
deps: SearchSessionDependencies,
user: AuthenticatedUser | null,
sessionId: string,
attributes: Partial<SearchSessionSavedObjectAttributes>
attributes: Partial<SearchSessionSavedObjectAttributes>,
skipRealmCheck: boolean = false
) => {
this.logger.debug(`SearchSessionService: update | ${sessionId}`);
if (!this.sessionConfig.enabled) throw new Error('Search sessions are disabled');
await this.get(deps, user, sessionId); // Verify correct user
await this.get(deps, user, sessionId, skipRealmCheck); // Verify correct user
return deps.savedObjectsClient.update<SearchSessionSavedObjectAttributes>(
SEARCH_SESSION_TYPE,
sessionId,
Expand Down Expand Up @@ -293,7 +297,8 @@ export class SearchSessionService implements ISearchSessionService {
deps: SearchSessionDependencies,
user: AuthenticatedUser | null,
searchId: string,
options: ISearchOptions
options: ISearchOptions,
skipRealmCheck: boolean = false
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since trackId is always called with skipRealmCheck: true does it make sense to even have this parameter?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lol, well I got the opposite feedback above #257497 (comment)

) => {
const { sessionId, strategy = ENHANCED_ES_SEARCH_STRATEGY, requestHash } = options;
if (!this.sessionConfig.enabled || !sessionId || !searchId) return;
Expand Down Expand Up @@ -328,7 +333,13 @@ export class SearchSessionService implements ISearchSessionService {
},
{}
);
this.update(queue[0].deps, queue[0].user, sessionId, { idMapping: batchedIdMapping })
this.update(
queue[0].deps,
queue[0].user,
sessionId,
{ idMapping: batchedIdMapping },
queue[0].skipRealmCheck
)
.then(() => {
queue.forEach((q) => q.resolve());
})
Expand All @@ -352,6 +363,7 @@ export class SearchSessionService implements ISearchSessionService {
resolve: deferred.resolve,
reject: deferred.reject,
user,
skipRealmCheck,
});

scheduleProcessQueue();
Expand Down Expand Up @@ -450,7 +462,8 @@ export class SearchSessionService implements ISearchSessionService {
deps: SearchSessionDependencies,
user: AuthenticatedUser | null,
searchRequest: IKibanaSearchRequest,
{ sessionId, isStored, isRestore, requestHash }: ISearchOptions
{ sessionId, isStored, isRestore, requestHash }: ISearchOptions,
skipRealmCheck: boolean = false
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same question as above, isn't this always called with true now?

) => {
if (!this.sessionConfig.enabled) {
throw new Error('Background search is disabled');
Expand All @@ -464,7 +477,7 @@ export class SearchSessionService implements ISearchSessionService {
throw new Error('Request hash is required to get search ID from session');
}

const session = await this.get(deps, user, sessionId);
const session = await this.get(deps, user, sessionId, skipRealmCheck);
if (!Object.hasOwn(session.attributes.idMapping, requestHash)) {
this.logger.debug(`SearchSessionService: getId | ${sessionId} | ${requestHash} not found`);
this.logger.error(
Expand Down Expand Up @@ -512,14 +525,32 @@ export class SearchSessionService implements ISearchSessionService {

private throwOnUserConflict = (
user: AuthenticatedUser | null,
session?: SavedObject<SearchSessionSavedObjectAttributes>
session?: SavedObject<SearchSessionSavedObjectAttributes>,
skipRealmCheck: boolean = false
) => {
if (user === null || !session) return;

if (skipRealmCheck) {
this.logger.debug(
`Skipping realm check for user ${user.username} when accessing search session ${session.attributes.sessionId}.`
);
}

let matchesUser = true;

if (user.username !== session.attributes.username) {
matchesUser = false;
}

if (
user.authentication_realm.type !== session.attributes.realmType ||
user.authentication_realm.name !== session.attributes.realmName ||
user.username !== session.attributes.username
!skipRealmCheck &&
(user.authentication_realm.type !== session.attributes.realmType ||
user.authentication_realm.name !== session.attributes.realmName)
) {
matchesUser = false;
}

if (!matchesUser) {
this.logger.debug(
`User ${user.username} has no access to search session ${session.attributes.sessionId}`
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,12 @@ import type { SearchSessionsConfigSchema } from '../../config';
export { SearchStatus } from '../../../common/search';

export interface IScopedSearchSessionsClient {
getId: (request: IKibanaSearchRequest, options: ISearchOptions) => Promise<string>;
trackId: (searchId: string, options: ISearchOptions) => Promise<void>;
getId: (
request: IKibanaSearchRequest,
options: ISearchOptions,
skipRealmCheck?: boolean
) => Promise<string>;
trackId: (searchId: string, options: ISearchOptions, skipRealmCheck?: boolean) => Promise<void>;
getSearchIdMapping: (sessionId: string) => Promise<Map<string, string>>;
save: (
sessionId: string,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,13 +183,6 @@ export class AuthorizationService {

http.registerOnPreResponse(async (request, preResponse, toolkit) => {
if (preResponse.statusCode === 403) {
const user = getCurrentUser(request);
if (user?.roles.length === 0) {
this.logger.warn(
`A user authenticated with the "${user.authentication_realm.name}" (${user.authentication_realm.type}) realm doesn't have any roles and isn't authorized to perform request.`
);
}
Comment on lines -186 to -191
Copy link
Copy Markdown
Contributor Author

@drewdaemon drewdaemon Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This log statement was creating a server error in some cases:

Error: Property "authentication_realm" is not available for minimally authenticated users.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

Context: "With minimal auth, we won’t always have roles available, so this message will become more and more misleading when debugging issues."


if (canRedirectRequest(request)) {
const next = `${http.basePath.get(request)}${request.url.pathname}${request.url.search}`;

Expand Down
Loading