Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { parse as parseCookie } from 'tough-cookie';

import { createSAMLResponse, MOCK_IDP_ATTRIBUTE_UIAM_ACCESS_TOKEN } from '@kbn/mock-idp-utils';
import { apiTest, tags } from '@kbn/scout';
import { expect } from '@kbn/scout/api';

import { COMMON_UNSAFE_HEADERS, extractAttributeValue } from '../fixtures';

// These tests cannot be run on MKI because we cannot obtain the raw UIAM tokens and spin up Mock IdP plugin.
apiTest.describe(
'[NON-MKI] UIAM API Keys grant and invalidate functions',
{ tag: [...tags.serverless.security.complete] },
() => {
let userSessionCookieFactory: () => Promise<[string, { accessToken: string }]>;

apiTest.beforeAll(async ({ apiClient, kbnUrl, config: { organizationId, projectType } }) => {
userSessionCookieFactory = async () => {
const samlResponse = await createSAMLResponse({
kibanaUrl: kbnUrl.get('/api/security/saml/callback'),
username: '1234567890',
email: 'elastic_admin@elastic.co',
roles: ['admin'],
serverless: {
uiamEnabled: true,
organizationId: organizationId!,
projectType: projectType!,
},
});

const decodedSamlResponse = Buffer.from(samlResponse, 'base64').toString('utf-8');
return [
parseCookie(
(
await apiClient.post('api/security/saml/callback', {
body: `SAMLResponse=${encodeURIComponent(samlResponse)}`,
})
).headers['set-cookie'][0]
)!.cookieString(),
{
accessToken: extractAttributeValue(
decodedSamlResponse,
MOCK_IDP_ATTRIBUTE_UIAM_ACCESS_TOKEN
),
},
];
};
});

apiTest(
'should be able to grant a UIAM API key with valid UIAM credentials',
async ({ apiClient }) => {
// 1. Log in to obtain a UIAM access token.
const [_, { accessToken }] = await userSessionCookieFactory();

// 2. Grant an API key using the UIAM access token via the test endpoint.
const responseUsingAccessToken = await apiClient.post(
'test_endpoints/uiam/api_keys/_grant',
{
headers: { ...COMMON_UNSAFE_HEADERS },
responseType: 'json',
body: {
name: 'test-uiam-api-key-from-token',
authcScheme: 'Bearer',
credential: accessToken,
},
}
);

expect(responseUsingAccessToken).toHaveStatusCode(200);
expect(responseUsingAccessToken.body.id).toBeDefined();
expect(responseUsingAccessToken.body.name).toBe('test-uiam-api-key-from-token');
expect(responseUsingAccessToken.body.api_key).toBeDefined();
expect(typeof responseUsingAccessToken.body.api_key).toBe('string');
expect(typeof responseUsingAccessToken.body.id).toBe('string');

// 3. UIAM API Keys should be able to grant additional UIAM API keys.
const apiKey = responseUsingAccessToken.body.api_key;

const responseUsingApiKey = await apiClient.post('test_endpoints/uiam/api_keys/_grant', {
headers: { ...COMMON_UNSAFE_HEADERS },
responseType: 'json',
body: {
name: 'test-uiam-api-key-from-api-key',
authcScheme: 'ApiKey',
credential: apiKey,
},
});

expect(responseUsingApiKey).toHaveStatusCode(200);
expect(responseUsingApiKey.body.id).toBeDefined();
expect(typeof responseUsingApiKey.body.id).toBe('string');
expect(responseUsingApiKey.body.name).toBe('test-uiam-api-key-from-api-key');
expect(responseUsingApiKey.body.api_key).toBeDefined();
expect(typeof responseUsingApiKey.body.api_key).toBe('string');
}
);

apiTest(
'should be able to invalidate a UIAM API key with valid UIAM credentials',
async ({ apiClient }) => {
// 1. Log in to obtain a UIAM access token.
const [_, { accessToken }] = await userSessionCookieFactory();

// 2. Grant an API key first.
const grantResponse = await apiClient.post('test_endpoints/uiam/api_keys/_grant', {
headers: { ...COMMON_UNSAFE_HEADERS },
responseType: 'json',
body: {
name: 'test-uiam-api-key-to-invalidate',
authcScheme: 'Bearer',
credential: accessToken,
},
});
expect(grantResponse).toHaveStatusCode(200);

const apiKeyId = grantResponse.body.id;
const apiKey = grantResponse.body.api_key;

// 3. Invalidate the API key.
const invalidateResponse = await apiClient.post(
'test_endpoints/uiam/api_keys/_invalidate',
{
headers: { ...COMMON_UNSAFE_HEADERS },
responseType: 'json',
body: {
id: apiKeyId,
authcScheme: 'ApiKey',
credential: apiKey,
},
}
);

expect(invalidateResponse).toHaveStatusCode(200);
expect(invalidateResponse.body).toStrictEqual(
expect.objectContaining({
invalidated_api_keys: [apiKeyId],
error_count: 0,
})
);
}
);

apiTest('should reject grant request with non-UIAM credentials', async ({ apiClient }) => {
// Attempt to grant an API key using non-UIAM credentials.
const response = await apiClient.post('test_endpoints/uiam/api_keys/_grant', {
headers: { ...COMMON_UNSAFE_HEADERS },
responseType: 'json',
body: {
name: 'test-invalid-api-key',
authcScheme: 'Bearer',
credential: 'some-invalid-token',
},
});

expect(response).toHaveStatusCode(500);
expect(response.body.message).toBeDefined();
expect(response.body.message).toContain('not compatible with UIAM');
});

apiTest('should reject invalidate request with non-UIAM credentials', async ({ apiClient }) => {
// Attempt to invalidate an API key using non UIAM API Key.
const response = await apiClient.post('test_endpoints/uiam/api_keys/_invalidate', {
headers: { ...COMMON_UNSAFE_HEADERS },
responseType: 'json',
body: {
id: 'some-api-key-id',
authcScheme: 'ApiKey',
credential: 'some-api-key-value:',
},
});

expect(response).toHaveStatusCode(500);
expect(response.body.message).toBeDefined();
expect(response.body.message).toContain('not a UIAM API key');
});
}
);
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ dependsOn:
- '@kbn/task-manager-plugin'
- '@kbn/config-schema'
- '@kbn/security-plugin-types-server'
- '@kbn/core-http-server'
- '@kbn/core-http-server-utils'
tags:
- plugin
- prod
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { type DiagnosticResult, errors } from '@elastic/elasticsearch';

import { schema } from '@kbn/config-schema';
import type { CoreSetup, CoreStart, PluginInitializerContext } from '@kbn/core/server';
import type { FakeRawRequest, Headers } from '@kbn/core-http-server';
import { kibanaRequestFactory } from '@kbn/core-http-server-utils';
import { ROUTE_TAG_AUTH_FLOW } from '@kbn/security-plugin/server';
import { restApiKeySchema } from '@kbn/security-plugin-types-server';
import type {
Expand Down Expand Up @@ -495,4 +497,122 @@ export function initRoutes(
}
}
);

// UIAM API Key Grant Route
router.post(
{
path: '/test_endpoints/uiam/api_keys/_grant',
validate: {
body: schema.object({
name: schema.string(),
expiration: schema.maybe(schema.string()),
authcScheme: schema.string(),
credential: schema.string(),
}),
},
security: {
authc: { enabled: 'optional' },
authz: { enabled: false, reason: 'Test endpoint for UIAM API key operations' },
},
},
async (context, request, response) => {
try {
const { name, expiration, authcScheme, credential } = request.body;
const [{ security }] = await core.getStartServices();

if (!security.authc.apiKeys.uiam) {
return response.badRequest({
body: { message: 'UIAM API keys service is not available' },
});
}

// Create a new request with the provided authentication header
const requestHeaders: Headers = {
...request.headers,
authorization: `${authcScheme} ${credential}`,
};
const fakeRawRequest: FakeRawRequest = {
headers: requestHeaders,
path: request.url.pathname,
};
const requestToUse = kibanaRequestFactory(fakeRawRequest);

const result = await security.authc.apiKeys.uiam.grant(requestToUse, {
name,
expiration,
});

if (!result) {
return response.badRequest({
body: { message: 'Failed to grant UIAM API key' },
});
}

return response.ok({ body: result });
} catch (err) {
logger.error(`Failed to grant UIAM API key: ${err}`, err);
return response.customError({
statusCode: 500,
body: { message: err.message },
});
}
}
);

// UIAM API Key Invalidate Route
router.post(
{
path: '/test_endpoints/uiam/api_keys/_invalidate',
validate: {
body: schema.object({
id: schema.string(),
authcScheme: schema.string(),
credential: schema.string(),
}),
},
security: {
authc: { enabled: 'optional' },
authz: { enabled: false, reason: 'Test endpoint for UIAM API key operations' },
},
},
async (context, request, response) => {
try {
const { id, authcScheme, credential } = request.body;
const [{ security }] = await core.getStartServices();

if (!security.authc.apiKeys.uiam) {
return response.badRequest({
body: { message: 'UIAM API keys service is not available' },
});
}

// Create a new request with the provided authentication header
const requestHeaders: Headers = {
...request.headers,
authorization: `${authcScheme} ${credential}`,
};
const fakeRawRequest: FakeRawRequest = {
headers: requestHeaders,
path: request.url.pathname,
};
const requestToUse = kibanaRequestFactory(fakeRawRequest);

const result = await security.authc.apiKeys.uiam.invalidate(requestToUse, { id });

if (!result) {
return response.badRequest({
body: { message: 'Failed to invalidate UIAM API key' },
});
}

return response.ok({ body: result });
} catch (err) {
logger.error(`Failed to invalidate UIAM API key: ${err}`, err);
return response.customError({
statusCode: 500,
body: { message: err.message },
});
}
}
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,7 @@
"@kbn/task-manager-plugin",
"@kbn/config-schema",
"@kbn/security-plugin-types-server",
"@kbn/core-http-server",
"@kbn/core-http-server-utils",
]
}
Loading