diff --git a/docs/user/security/audit-logging.asciidoc b/docs/user/security/audit-logging.asciidoc index ce1e63c20d1c6..5f6fe746814e5 100644 --- a/docs/user/security/audit-logging.asciidoc +++ b/docs/user/security/audit-logging.asciidoc @@ -325,6 +325,9 @@ Possible values: | *Field* | *Description* +| `user.id` +| Unique identifier of the user across sessions (See {ref}/user-profile.html[user profiles]). + | `user.name` | Login name of the user. diff --git a/x-pack/plugins/security/common/model/authenticated_user.mock.ts b/x-pack/plugins/security/common/model/authenticated_user.mock.ts index 73641d2fa5983..84b300d5c982b 100644 --- a/x-pack/plugins/security/common/model/authenticated_user.mock.ts +++ b/x-pack/plugins/security/common/model/authenticated_user.mock.ts @@ -24,6 +24,7 @@ export function mockAuthenticatedUser(user: MockAuthenticatedUserProps = {}) { authentication_provider: { type: 'basic', name: 'basic1' }, authentication_type: 'realm', elastic_cloud_user: false, + profile_uid: 'uid', metadata: { _reserved: false }, ...user, }; diff --git a/x-pack/plugins/security/common/model/authenticated_user.ts b/x-pack/plugins/security/common/model/authenticated_user.ts index 7f7e965994e4b..fd78b250a5ccc 100644 --- a/x-pack/plugins/security/common/model/authenticated_user.ts +++ b/x-pack/plugins/security/common/model/authenticated_user.ts @@ -57,6 +57,11 @@ export interface AuthenticatedUser extends User { * Indicates whether user is authenticated via Elastic Cloud built-in SAML realm. */ elastic_cloud_user: boolean; + + /** + * User profile ID of this user. + */ + profile_uid?: string; } export function isUserAnonymous(user: Pick) { diff --git a/x-pack/plugins/security/server/audit/audit_events.test.ts b/x-pack/plugins/security/server/audit/audit_events.test.ts index 73523b4a3e009..1f9ab461e0b00 100644 --- a/x-pack/plugins/security/server/audit/audit_events.test.ts +++ b/x-pack/plugins/security/server/audit/audit_events.test.ts @@ -240,6 +240,7 @@ describe('#userLoginEvent', () => { authenticationProvider: 'basic1', authenticationType: 'basic', sessionId: '123', + userProfileId: 'uid', }) ).toMatchInlineSnapshot(` Object { @@ -261,6 +262,7 @@ describe('#userLoginEvent', () => { }, "message": "User [user] has logged in using basic provider [name=basic1]", "user": Object { + "id": "uid", "name": "user", "roles": Array [ "user-role", @@ -311,6 +313,7 @@ describe('#userLogoutEvent', () => { userLogoutEvent({ username: 'elastic', provider: { name: 'basic1', type: 'basic' }, + userProfileId: 'uid', }) ).toMatchInlineSnapshot(` Object { @@ -327,6 +330,7 @@ describe('#userLogoutEvent', () => { }, "message": "User [elastic] is logging out using basic provider [name=basic1]", "user": Object { + "id": "uid", "name": "elastic", }, } diff --git a/x-pack/plugins/security/server/audit/audit_events.ts b/x-pack/plugins/security/server/audit/audit_events.ts index dd767df83b692..deb4b356c9f95 100644 --- a/x-pack/plugins/security/server/audit/audit_events.ts +++ b/x-pack/plugins/security/server/audit/audit_events.ts @@ -98,6 +98,7 @@ export interface UserLoginParams { authenticationProvider?: string; authenticationType?: string; sessionId?: string; + userProfileId?: string; } export function userLoginEvent({ @@ -105,6 +106,7 @@ export function userLoginEvent({ authenticationProvider, authenticationType, sessionId, + userProfileId, }: UserLoginParams): AuditEvent { return { message: authenticationResult.user @@ -116,6 +118,7 @@ export function userLoginEvent({ outcome: authenticationResult.user ? 'success' : 'failure', }, user: authenticationResult.user && { + id: userProfileId, name: authenticationResult.user.username, roles: authenticationResult.user.roles as string[], }, @@ -137,9 +140,14 @@ export function userLoginEvent({ export interface UserLogoutParams { username?: string; provider: AuthenticationProvider; + userProfileId?: string; } -export function userLogoutEvent({ username, provider }: UserLogoutParams): AuditEvent { +export function userLogoutEvent({ + username, + provider, + userProfileId, +}: UserLogoutParams): AuditEvent { return { message: `User [${username}] is logging out using ${provider.type} provider [name=${provider.name}]`, event: { @@ -147,11 +155,13 @@ export function userLogoutEvent({ username, provider }: UserLogoutParams): Audit category: ['authentication'], outcome: 'unknown', }, - user: username - ? { - name: username, - } - : undefined, + user: + userProfileId || username + ? { + id: userProfileId, + name: username, + } + : undefined, kibana: { authentication_provider: provider.name, authentication_type: provider.type, diff --git a/x-pack/plugins/security/server/audit/audit_service.test.ts b/x-pack/plugins/security/server/audit/audit_service.test.ts index d750fa2fc36b9..dfd42c2260c5e 100644 --- a/x-pack/plugins/security/server/audit/audit_service.test.ts +++ b/x-pack/plugins/security/server/audit/audit_service.test.ts @@ -38,7 +38,9 @@ const createAuditConfig = (settings: Partial) => { const config = createAuditConfig({ enabled: true }); const { logging } = coreMock.createSetup(); const http = httpServiceMock.createSetupContract(); -const getCurrentUser = jest.fn().mockReturnValue({ username: 'jdoe', roles: ['admin'] }); +const getCurrentUser = jest + .fn() + .mockReturnValue({ username: 'jdoe', roles: ['admin'], profile_uid: 'uid' }); const getSpaceId = jest.fn().mockReturnValue('default'); const getSID = jest.fn().mockResolvedValue('SESSION_ID'); const recordAuditLoggingUsage = jest.fn(); @@ -192,7 +194,7 @@ describe('#asScoped', () => { event: { action: 'ACTION' }, kibana: { space_id: 'default', session_id: 'SESSION_ID' }, trace: { id: 'REQUEST_ID' }, - user: { name: 'jdoe', roles: ['admin'] }, + user: { id: 'uid', name: 'jdoe', roles: ['admin'] }, }); audit.stop(); }); diff --git a/x-pack/plugins/security/server/audit/audit_service.ts b/x-pack/plugins/security/server/audit/audit_service.ts index 89b288d07e9fe..ff8a09df40198 100644 --- a/x-pack/plugins/security/server/audit/audit_service.ts +++ b/x-pack/plugins/security/server/audit/audit_service.ts @@ -166,6 +166,7 @@ export class AuditService { ...event, user: (user && { + id: user.profile_uid, name: user.username, roles: user.roles as string[], }) || diff --git a/x-pack/plugins/security/server/authentication/authentication_service.test.ts b/x-pack/plugins/security/server/authentication/authentication_service.test.ts index 0f2657a419a0a..55357be756e7e 100644 --- a/x-pack/plugins/security/server/authentication/authentication_service.test.ts +++ b/x-pack/plugins/security/server/authentication/authentication_service.test.ts @@ -273,6 +273,33 @@ describe('AuthenticationService', () => { expect(authenticate).toHaveBeenCalledWith(mockRequest); }); + it('sets authenticated state correctly with user profile id', async () => { + const mockRequest = httpServerMock.createKibanaRequest(); + const mockResponse = httpServerMock.createLifecycleResponseFactory(); + const mockUser = mockAuthenticatedUser(); + const mockAuthHeaders = { authorization: 'Basic xxx' }; + const mockAuthResponseHeaders = { 'WWW-Authenticate': 'Negotiate' }; + + authenticate.mockResolvedValue( + AuthenticationResult.succeeded( + { ...mockUser, profile_uid: 'USER_PROFILE_ID' }, + { + authHeaders: mockAuthHeaders, + authResponseHeaders: mockAuthResponseHeaders, + } + ) + ); + + await authHandler(mockRequest, mockResponse, mockAuthToolkit); + + expect(mockAuthToolkit.authenticated).toHaveBeenCalledTimes(1); + expect(mockAuthToolkit.authenticated).toHaveBeenCalledWith({ + state: { ...mockUser, profile_uid: 'USER_PROFILE_ID' }, + requestHeaders: mockAuthHeaders, + responseHeaders: mockAuthResponseHeaders, + }); + }); + it('redirects user if redirection is requested by the authenticator preserving authentication response headers if any', async () => { const mockResponse = httpServerMock.createLifecycleResponseFactory(); authenticate.mockResolvedValue( diff --git a/x-pack/plugins/security/server/authentication/authenticator.test.ts b/x-pack/plugins/security/server/authentication/authenticator.test.ts index 1c62b5ae44dd9..1c160bb4dfa48 100644 --- a/x-pack/plugins/security/server/authentication/authenticator.test.ts +++ b/x-pack/plugins/security/server/authentication/authenticator.test.ts @@ -39,7 +39,7 @@ import type { UserProfileGrant } from '../user_profile'; import { userProfileServiceMock } from '../user_profile/user_profile_service.mock'; import { AuthenticationResult } from './authentication_result'; import type { AuthenticatorOptions } from './authenticator'; -import { Authenticator } from './authenticator'; +import { Authenticator, enrichWithUserProfileId } from './authenticator'; import { DeauthenticationResult } from './deauthentication_result'; import type { BasicAuthenticationProvider, SAMLAuthenticationProvider } from './providers'; @@ -379,6 +379,29 @@ describe('Authenticator', () => { expectAuditEvents({ action: 'user_login', outcome: 'success' }); }); + it('returns user enriched with user profile id.', async () => { + const request = httpServerMock.createKibanaRequest(); + const user = mockAuthenticatedUser({ profile_uid: undefined }); + mockOptions.session.create.mockResolvedValue( + sessionMock.createValue({ + userProfileId: 'PROFILE_ID', + }) + ); + + mockBasicAuthenticationProvider.login.mockResolvedValue( + AuthenticationResult.succeeded(user, { + state: {}, // to ensure a new session is created + }) + ); + + const result = await authenticator.login(request, { provider: { type: 'basic' }, value: {} }); + expect(result.user).toEqual( + expect.objectContaining({ + profile_uid: 'PROFILE_ID', + }) + ); + }); + describe('user_login audit events', () => { // Every other test case includes audit event assertions, but the user_login event is a bit special. // We have these separate, detailed test cases to ensure that the session ID is included for user_login success events. @@ -2560,3 +2583,65 @@ describe('Authenticator', () => { }); }); }); + +describe('enrichWithUserProfileId', () => { + it('should enrich succeeded authentication results with user profile id', () => { + const authenticationResult = AuthenticationResult.succeeded( + mockAuthenticatedUser({ profile_uid: undefined }) + ); + const sessionValue = sessionMock.createValue({ userProfileId: 'uid' }); + expect(enrichWithUserProfileId(authenticationResult, sessionValue)).toEqual( + expect.objectContaining({ + user: expect.objectContaining({ + profile_uid: 'uid', + }), + }) + ); + }); + + it('should enrich redirected authentication results with user profile id', () => { + const authenticationResult = AuthenticationResult.redirectTo('/redirect/to', { + user: mockAuthenticatedUser({ profile_uid: undefined }), + }); + const sessionValue = sessionMock.createValue({ userProfileId: 'uid' }); + expect(enrichWithUserProfileId(authenticationResult, sessionValue)).toEqual( + expect.objectContaining({ + user: expect.objectContaining({ + profile_uid: 'uid', + }), + }) + ); + }); + + it('should not change unhandled authentication results', () => { + const authenticationResult = AuthenticationResult.notHandled(); + const sessionValue = sessionMock.createValue(); + expect(enrichWithUserProfileId(authenticationResult, sessionValue)).toBe(authenticationResult); + }); + + it('should not change failed authentication results', () => { + const authenticationResult = AuthenticationResult.failed(new Error('Authentication error')); + const sessionValue = sessionMock.createValue(); + expect(enrichWithUserProfileId(authenticationResult, sessionValue)).toBe(authenticationResult); + }); + + it('should not change redirected authentication results without user', () => { + const authenticationResult = AuthenticationResult.redirectTo('/redirect/to'); + const sessionValue = sessionMock.createValue(); + expect(enrichWithUserProfileId(authenticationResult, sessionValue)).toBe(authenticationResult); + }); + + it('should not change succeeded authentication result if session has no user profile id', () => { + const authenticationResult = AuthenticationResult.succeeded(mockAuthenticatedUser()); + const sessionValue = sessionMock.createValue({ userProfileId: undefined }); + expect(enrichWithUserProfileId(authenticationResult, sessionValue)).toBe(authenticationResult); + }); + + it('should not change succeeded authentication result if user profile ids already match', () => { + const authenticationResult = AuthenticationResult.succeeded( + mockAuthenticatedUser({ profile_uid: 'uid' }) + ); + const sessionValue = sessionMock.createValue({ userProfileId: 'uid' }); + expect(enrichWithUserProfileId(authenticationResult, sessionValue)).toBe(authenticationResult); + }); +}); diff --git a/x-pack/plugins/security/server/authentication/authenticator.ts b/x-pack/plugins/security/server/authentication/authenticator.ts index 735224fd83720..0e925530d10a6 100644 --- a/x-pack/plugins/security/server/authentication/authenticator.ts +++ b/x-pack/plugins/security/server/authentication/authenticator.ts @@ -335,11 +335,14 @@ export class Authenticator { existingSessionValue, }); - return this.handlePreAccessRedirects( - request, - authenticationResult, - sessionUpdateResult, - attempt.redirectURL + return enrichWithUserProfileId( + this.handlePreAccessRedirects( + request, + authenticationResult, + sessionUpdateResult, + attempt.redirectURL + ), + sessionUpdateResult ? sessionUpdateResult.value : null ); } } @@ -351,7 +354,7 @@ export class Authenticator { * Performs request authentication using configured chain of authentication providers. * @param request Request instance. */ - async authenticate(request: KibanaRequest) { + async authenticate(request: KibanaRequest): Promise { assertRequest(request); const existingSessionValue = await this.getSessionValue(request); @@ -399,10 +402,12 @@ export class Authenticator { authenticationResult, existingSessionValue, }); - - return canRedirectRequest(request) - ? this.handlePreAccessRedirects(request, authenticationResult, sessionUpdateResult) - : authenticationResult; + return enrichWithUserProfileId( + canRedirectRequest(request) + ? this.handlePreAccessRedirects(request, authenticationResult, sessionUpdateResult) + : authenticationResult, + sessionUpdateResult ? sessionUpdateResult.value : null + ); } } @@ -427,11 +432,15 @@ export class Authenticator { const provider = this.providers.get(existingSessionValue.provider.name)!; const authenticationResult = await provider.authenticate(request, existingSessionValue.state); if (!authenticationResult.notHandled()) { - await this.updateSessionValue(request, { + const sessionUpdateResult = await this.updateSessionValue(request, { provider: existingSessionValue.provider, authenticationResult, existingSessionValue, }); + + if (sessionUpdateResult) { + return enrichWithUserProfileId(authenticationResult, sessionUpdateResult.value); + } } return authenticationResult; @@ -631,6 +640,7 @@ export class Authenticator { const auditLogger = this.options.audit.asScoped(request); auditLogger.log( userLoginEvent({ + userProfileId: existingSessionValue?.userProfileId, sessionId: existingSessionValue?.sid, authenticationResult, authenticationProvider: provider.name, @@ -737,7 +747,7 @@ export class Authenticator { } } - let newSessionValue; + let newSessionValue: Readonly | null; if (!existingSessionValue) { newSessionValue = await this.session.create(request, { username: authenticationResult.user?.username, @@ -756,6 +766,7 @@ export class Authenticator { const auditLogger = this.options.audit.asScoped(request); auditLogger.log( userLoginEvent({ + userProfileId, // We must explicitly specify the `userProfileId` here since we just created the session and it can't be inferred from the request context. sessionId: newSessionValue?.sid, // We must explicitly specify the `sessionId` here since we just created the session and it can't be inferred from the request context. authenticationResult, authenticationProvider: provider.name, @@ -796,12 +807,7 @@ export class Authenticator { }: InvalidateSessionValueParams) { if (isSessionAuthenticated(sessionValue) && !skipAuditEvent) { const auditLogger = this.options.audit.asScoped(request); - auditLogger.log( - userLogoutEvent({ - username: sessionValue.username, - provider: sessionValue.provider, - }) - ); + auditLogger.log(userLogoutEvent(sessionValue)); } await this.session.invalidate(request, { match: 'current' }); @@ -946,3 +952,37 @@ export class Authenticator { : `${this.options.basePath.serverBasePath}/security/logged_out?${searchParams.toString()}`; } } + +export function enrichWithUserProfileId( + authenticationResult: AuthenticationResult, + sessionValue: SessionValue | null +) { + if ( + !authenticationResult.user || + !sessionValue?.userProfileId || + authenticationResult.user.profile_uid === sessionValue.userProfileId + ) { + return authenticationResult; + } + + const enrichedUser: AuthenticatedUser = { + ...authenticationResult.user, + profile_uid: sessionValue.userProfileId, + }; + + if (authenticationResult.redirected()) { + return AuthenticationResult.redirectTo(authenticationResult.redirectURL!, { + user: enrichedUser, + userProfileGrant: authenticationResult.userProfileGrant, + authResponseHeaders: authenticationResult.authResponseHeaders, + state: authenticationResult.state, + }); + } + + return AuthenticationResult.succeeded(enrichedUser, { + userProfileGrant: authenticationResult.userProfileGrant, + authHeaders: authenticationResult.authHeaders, + authResponseHeaders: authenticationResult.authResponseHeaders, + state: authenticationResult.state, + }); +} diff --git a/x-pack/test/api_integration/apis/security/basic_login.js b/x-pack/test/api_integration/apis/security/basic_login.js index c81034b6fb824..6402945af74a6 100644 --- a/x-pack/test/api_integration/apis/security/basic_login.js +++ b/x-pack/test/api_integration/apis/security/basic_login.js @@ -135,7 +135,7 @@ export default function ({ getService }) { ) .expect(200); - expect(apiResponse.body).to.only.have.keys([ + expect(apiResponse.body).to.have.keys([ 'username', 'full_name', 'email', @@ -182,7 +182,7 @@ export default function ({ getService }) { .set('Cookie', sessionCookie.cookieString()) .expect(200); - expect(apiResponse.body).to.only.have.keys([ + expect(apiResponse.body).to.have.keys([ 'username', 'full_name', 'email', diff --git a/x-pack/test/security_api_integration/tests/kerberos/kerberos_login.ts b/x-pack/test/security_api_integration/tests/kerberos/kerberos_login.ts index f92f9d5a58b5b..10420d637afaf 100644 --- a/x-pack/test/security_api_integration/tests/kerberos/kerberos_login.ts +++ b/x-pack/test/security_api_integration/tests/kerberos/kerberos_login.ts @@ -6,6 +6,7 @@ */ import expect from '@kbn/expect'; +import jestExpect from 'expect'; import { parse as parseCookie, Cookie } from 'tough-cookie'; import { setTimeout as setTimeoutAsync } from 'timers/promises'; import { adminTestUser } from '@kbn/test'; @@ -142,26 +143,29 @@ export default function ({ getService }: FtrProviderContext) { ? ['kibana_admin', 'superuser_anonymous'] : ['kibana_admin']; - await supertest + const spnegoResponse = await supertest .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Cookie', sessionCookie.cookieString()) - .expect(200, { - username: 'tester@TEST.ELASTIC.CO', - roles: expectedUserRoles, - full_name: null, - email: null, - metadata: { - kerberos_user_principal_name: 'tester@TEST.ELASTIC.CO', - kerberos_realm: 'TEST.ELASTIC.CO', - }, - enabled: true, - authentication_realm: { name: 'kerb1', type: 'kerberos' }, - lookup_realm: { name: 'kerb1', type: 'kerberos' }, - authentication_provider: { type: 'kerberos', name: 'kerberos' }, - authentication_type: 'token', - elastic_cloud_user: false, - }); + .expect(200); + + jestExpect(spnegoResponse.body).toEqual({ + username: 'tester@TEST.ELASTIC.CO', + roles: expectedUserRoles, + full_name: null, + email: null, + metadata: { + kerberos_user_principal_name: 'tester@TEST.ELASTIC.CO', + kerberos_realm: 'TEST.ELASTIC.CO', + }, + enabled: true, + authentication_realm: { name: 'kerb1', type: 'kerberos' }, + lookup_realm: { name: 'kerb1', type: 'kerberos' }, + authentication_provider: { type: 'kerberos', name: 'kerberos' }, + authentication_type: 'token', + elastic_cloud_user: false, + profile_uid: jestExpect.any(String), + }); }); it('should re-initiate SPNEGO handshake if token is rejected with 401', async () => { diff --git a/x-pack/test/security_api_integration/tests/login_selector/basic_functionality.ts b/x-pack/test/security_api_integration/tests/login_selector/basic_functionality.ts index df7d309261a38..b9f5aa96080e9 100644 --- a/x-pack/test/security_api_integration/tests/login_selector/basic_functionality.ts +++ b/x-pack/test/security_api_integration/tests/login_selector/basic_functionality.ts @@ -53,7 +53,7 @@ export default function ({ getService }: FtrProviderContext) { .set('Cookie', sessionCookie.cookieString()) .expect(200); - expect(apiResponse.body).to.only.have.keys([ + expect(apiResponse.body).to.have.keys([ 'username', 'full_name', 'email', diff --git a/x-pack/test/security_api_integration/tests/oidc/authorization_code_flow/oidc_auth.ts b/x-pack/test/security_api_integration/tests/oidc/authorization_code_flow/oidc_auth.ts index 9ea2e9b635522..4222f4f6a64f7 100644 --- a/x-pack/test/security_api_integration/tests/oidc/authorization_code_flow/oidc_auth.ts +++ b/x-pack/test/security_api_integration/tests/oidc/authorization_code_flow/oidc_auth.ts @@ -217,7 +217,7 @@ export default function ({ getService }: FtrProviderContext) { .set('kbn-xsrf', 'xxx') .set('Cookie', sessionCookie.cookieString()) .expect(200); - expect(apiResponse.body).to.only.have.keys([ + expect(apiResponse.body).to.have.keys([ 'username', 'full_name', 'email', @@ -271,7 +271,7 @@ export default function ({ getService }: FtrProviderContext) { .set('kbn-xsrf', 'xxx') .set('Cookie', sessionCookie.cookieString()) .expect(200); - expect(apiResponse.body).to.only.have.keys([ + expect(apiResponse.body).to.have.keys([ 'username', 'full_name', 'email', diff --git a/x-pack/test/security_api_integration/tests/oidc/implicit_flow/oidc_auth.ts b/x-pack/test/security_api_integration/tests/oidc/implicit_flow/oidc_auth.ts index 9a0483d5cebaa..8e7fe36fd689b 100644 --- a/x-pack/test/security_api_integration/tests/oidc/implicit_flow/oidc_auth.ts +++ b/x-pack/test/security_api_integration/tests/oidc/implicit_flow/oidc_auth.ts @@ -141,7 +141,7 @@ export default function ({ getService }: FtrProviderContext) { .set('kbn-xsrf', 'xxx') .set('Cookie', sessionCookie.cookieString()) .expect(200); - expect(apiResponse.body).to.only.have.keys([ + expect(apiResponse.body).to.have.keys([ 'username', 'full_name', 'email', diff --git a/x-pack/test/security_api_integration/tests/pki/pki_auth.ts b/x-pack/test/security_api_integration/tests/pki/pki_auth.ts index 6b8178ac938df..c286adcc7879b 100644 --- a/x-pack/test/security_api_integration/tests/pki/pki_auth.ts +++ b/x-pack/test/security_api_integration/tests/pki/pki_auth.ts @@ -6,6 +6,7 @@ */ import expect from '@kbn/expect'; +import jestExpect from 'expect'; import { parse as parseCookie, Cookie } from 'tough-cookie'; import { setTimeout as setTimeoutAsync } from 'timers/promises'; import { readFileSync } from 'fs'; @@ -123,7 +124,7 @@ export default function ({ getService }: FtrProviderContext) { }); it('should properly set cookie and authenticate user', async () => { - const response = await supertest + let response = await supertest .get('/security/account') .ca(CA_CERT) .pfx(FIRST_CLIENT_CERT) @@ -136,29 +137,32 @@ export default function ({ getService }: FtrProviderContext) { checkCookieIsSet(sessionCookie); // Cookie should be accepted. - await supertest + response = await supertest .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .ca(CA_CERT) .pfx(FIRST_CLIENT_CERT) .set('Cookie', sessionCookie.cookieString()) - .expect(200, { - username: 'first_client', - roles: ['kibana_admin'], - full_name: null, - email: null, - enabled: true, - metadata: { - pki_delegated_by_realm: 'reserved', - pki_delegated_by_user: 'kibana_system', - pki_dn: 'CN=first_client', - }, - authentication_realm: { name: 'pki1', type: 'pki' }, - lookup_realm: { name: 'pki1', type: 'pki' }, - authentication_provider: { name: 'pki', type: 'pki' }, - authentication_type: 'token', - elastic_cloud_user: false, - }); + .expect(200); + + jestExpect(response.body).toEqual({ + username: 'first_client', + roles: ['kibana_admin'], + full_name: null, + email: null, + enabled: true, + metadata: { + pki_delegated_by_realm: 'reserved', + pki_delegated_by_user: 'kibana_system', + pki_dn: 'CN=first_client', + }, + authentication_realm: { name: 'pki1', type: 'pki' }, + lookup_realm: { name: 'pki1', type: 'pki' }, + authentication_provider: { name: 'pki', type: 'pki' }, + authentication_type: 'token', + elastic_cloud_user: false, + profile_uid: jestExpect.any(String), + }); }); it('should update session if new certificate is provided', async () => { @@ -180,23 +184,26 @@ export default function ({ getService }: FtrProviderContext) { .pfx(SECOND_CLIENT_CERT) .set('kbn-xsrf', 'xxx') .set('Cookie', sessionCookie.cookieString()) - .expect(200, { - username: 'second_client', - roles: [], - full_name: null, - email: null, - enabled: true, - metadata: { - pki_delegated_by_realm: 'reserved', - pki_delegated_by_user: 'kibana_system', - pki_dn: 'CN=second_client', - }, - authentication_realm: { name: 'pki1', type: 'pki' }, - lookup_realm: { name: 'pki1', type: 'pki' }, - authentication_provider: { name: 'pki', type: 'pki' }, - authentication_type: 'realm', - elastic_cloud_user: false, - }); + .expect(200); + + jestExpect(response.body).toEqual({ + username: 'second_client', + roles: [], + full_name: null, + email: null, + enabled: true, + metadata: { + pki_delegated_by_realm: 'reserved', + pki_delegated_by_user: 'kibana_system', + pki_dn: 'CN=second_client', + }, + authentication_realm: { name: 'pki1', type: 'pki' }, + lookup_realm: { name: 'pki1', type: 'pki' }, + authentication_provider: { name: 'pki', type: 'pki' }, + authentication_type: 'realm', + elastic_cloud_user: false, + profile_uid: jestExpect.any(String), + }); checkCookieIsSet(parseCookie(response.headers['set-cookie'][0])!); }); diff --git a/x-pack/test/security_api_integration/tests/saml/saml_login.ts b/x-pack/test/security_api_integration/tests/saml/saml_login.ts index 474525eab4979..43d38a818c8ff 100644 --- a/x-pack/test/security_api_integration/tests/saml/saml_login.ts +++ b/x-pack/test/security_api_integration/tests/saml/saml_login.ts @@ -55,7 +55,7 @@ export default function ({ getService }: FtrProviderContext) { .set('Cookie', sessionCookie.cookieString()) .expect(200); - expect(apiResponse.body).to.only.have.keys([ + expect(apiResponse.body).to.have.keys([ 'username', 'full_name', 'email', diff --git a/x-pack/test/security_api_integration/tests/user_profiles/get_current.ts b/x-pack/test/security_api_integration/tests/user_profiles/get_current.ts index d7f23545aecd0..a56c5ac9004b8 100644 --- a/x-pack/test/security_api_integration/tests/user_profiles/get_current.ts +++ b/x-pack/test/security_api_integration/tests/user_profiles/get_current.ts @@ -6,6 +6,7 @@ */ import { parse as parseCookie } from 'tough-cookie'; +import expect from 'expect'; import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { @@ -64,6 +65,10 @@ export default function ({ getService }: FtrProviderContext) { .get('/internal/security/user_profile?dataPath=some') .set('Cookie', sessionCookie.cookieString()) .expect(200); + const { body: userWithProfileId } = await supertestWithoutAuth + .get('/internal/security/me') + .set('Cookie', sessionCookie.cookieString()) + .expect(200); // Profile UID is supposed to be stable. expectSnapshot(profileWithoutData).toMatchInline(` @@ -134,6 +139,7 @@ export default function ({ getService }: FtrProviderContext) { }, } `); + expect(userWithProfileId.profile_uid).toBe('u_K1WXIRQbRoHiuJylXp842IEhAO_OdqT7SDHrJSzUIjU_0'); }); }); }