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
Expand Up @@ -5,7 +5,11 @@
* 2.0.
*/

import { validateHost, validateDownloadSourceHeaders } from './use_download_source_flyout_form';
import {
validateHost,
validateDownloadSourceHeaders,
type AuthType,
} from './use_download_source_flyout_form';

describe('Download source form validation', () => {
describe('validateHost', () => {
Expand Down Expand Up @@ -56,6 +60,65 @@ describe('Download source form validation', () => {
expect(res).toBeUndefined();
});

it('should return undefined when Authorization header is present with authType "none"', () => {
const res = validateDownloadSourceHeaders(
[{ key: 'Authorization', value: 'Bearer token' }],
'none'
);

expect(res).toBeUndefined();
});

it.each<AuthType>(['username_password', 'api_key'])(
'should return error when Authorization header conflicts with %s authType',
(authType) => {
const res = validateDownloadSourceHeaders(
[
{ key: 'X-Custom-Header', value: 'custom-value' },
{ key: 'Authorization', value: 'Bearer token' },
],
authType
);

expect(res).toEqual([
{
message:
'Cannot use "Authorization" header when credentials are configured. The credentials will overwrite this header.',
index: 1,
hasKeyError: true,
hasValueError: false,
},
]);
}
);

it('should return error when Authorization header conflicts with api_key authType', () => {
const res = validateDownloadSourceHeaders(
[{ key: 'Authorization', value: 'Bearer token' }],
'api_key'
);

expect(res).toEqual([
{
message:
'Cannot use "Authorization" header when credentials are configured. The credentials will overwrite this header.',
index: 0,
hasKeyError: true,
hasValueError: false,
},
]);
});

it('should return error for Authorization header regardless of case when credentials are set', () => {
const res = validateDownloadSourceHeaders(
[{ key: 'authorization', value: 'Bearer token' }],
'api_key'
);

expect(res).toHaveLength(1);
expect(res![0]).toMatchObject({ index: 0, hasKeyError: true });
});

it('should return undefined for empty headers (both key and value empty)', () => {
const res = validateDownloadSourceHeaders([{ key: '', value: '' }]);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* 2.0.
*/

import { useCallback, useState } from 'react';
import { useCallback, useMemo, useState } from 'react';

import { i18n } from '@kbn/i18n';

Expand Down Expand Up @@ -121,10 +121,16 @@ export function useDowloadSourceFlyoutForm(onSuccess: () => void, downloadSource
isEditDisabled
);

const validateHeadersWithAuthType = useMemo(
() => (pairs: Array<{ key: string; value: string }>) =>
validateDownloadSourceHeaders(pairs, authTypeInput.value as AuthType),
[authTypeInput.value]
);

const headersInput = useKeyValueInput(
'downloadSourceHeadersInput',
(downloadSource as DownloadSourceBase)?.auth?.headers ?? [{ key: '', value: '' }],
validateDownloadSourceHeaders,
validateHeadersWithAuthType,
isEditDisabled
);

Expand Down Expand Up @@ -391,7 +397,10 @@ export function validateHost(value: string) {
}
}

export function validateDownloadSourceHeaders(pairs: Array<{ key: string; value: string }>) {
export function validateDownloadSourceHeaders(
pairs: Array<{ key: string; value: string }>,
authType?: AuthType
) {
const errors: Array<{
message: string;
index: number;
Expand Down Expand Up @@ -450,6 +459,21 @@ export function validateDownloadSourceHeaders(pairs: Array<{ key: string; value:
} else {
existingKeys.add(key);
}

if (authType && authType !== 'none' && key.toLowerCase() === 'authorization') {
errors.push({
message: i18n.translate(
'xpack.fleet.settings.dowloadSourceFlyoutForm.headersAuthorizationConflictError',
{
defaultMessage:
'Cannot use "Authorization" header when credentials are configured. The credentials will overwrite this header.',
}
),
index,
hasKeyError: true,
hasValueError: false,
});
}
}
});
if (errors.length) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,18 @@ export function validateDownloadSource(downloadSource: DownloadSourceWithNullabl
if (hasPassword && !hasUsername) {
throw Boom.badRequest('Username and password must be provided together');
}

// Disallow "Authorization" custom header when credentials (username/password or api_key) are configured,
// as the credentials would overwrite the header value.
const hasCredentials = Boolean(hasUsernameOrPassword || hasApiKey);
const hasAuthorizationHeader = downloadSource.auth?.headers?.some(
(header) => header.key.toLowerCase() === 'authorization'
);
if (hasCredentials && hasAuthorizationHeader) {
throw Boom.badRequest(
'Cannot use "Authorization" custom header when username/password or api_key authentication is configured'
);
}
}

export const getDownloadSourcesHandler: RequestHandler = async (context, request, response) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,16 @@ export default function (providerContext: FtrProviderContext) {
}
};

const authHeaderConflictCases: Array<[string, { auth?: {}; secrets?: {} }]> = [
['username/password', { auth: { username: 'testuser', password: 'testpassword' } }],
[
'username/password as secret',
{ auth: { username: 'testuser' }, secrets: { auth: { password: 'testpassword' } } },
],
['api_key', { auth: { api_key: 'my-api-key' } }],
['api_key as secret', { secrets: { auth: { api_key: 'my-api-key' } } }],
];

describe('fleet_download_sources_crud', function () {
let defaultDownloadSourceId: string;
let fleetServerPolicyId: string;
Expand Down Expand Up @@ -505,6 +515,28 @@ export default function (providerContext: FtrProviderContext) {
);
});

authHeaderConflictCases.forEach(([description, { auth, secrets }]) => {
it(`should return 400 when Authorization header is combined with ${description} credentials on create`, async function () {
const baseAuth = {
headers: [{ key: 'Authorization', value: 'Bearer token' }],
};

const res = await supertest
.post(`/api/fleet/agent_download_sources`)
.set('kbn-xsrf', 'xxxx')
.send({
name: `Download source auth conflict ${Date.now()}`,
host: 'http://test.fr:443',
is_default: false,
auth: { ...baseAuth, ...auth },
...(secrets ? { secrets } : {}),
})
.expect(400);

expect(res.body.message).to.contain('Cannot use "Authorization" custom header');
});
});

it('should store auth secrets when fleet server meets minimum version', async function () {
await clearAgents();
await createFleetServerAgent(fleetServerPolicyId, 'server_1', '9.4.0');
Expand Down Expand Up @@ -792,6 +824,38 @@ export default function (providerContext: FtrProviderContext) {
.expect(400);
});

authHeaderConflictCases.forEach(([description, { auth, secrets }]) => {
it(`should return 400 when Authorization header is combined with ${description} credentials on update`, async function () {
const { body: createRes } = await supertest
.post(`/api/fleet/agent_download_sources`)
.set('kbn-xsrf', 'xxxx')
.send({
name: `Bad auth combination test ${Date.now()}`,
host: 'http://test.fr:443',
is_default: false,
})
.expect(200);

const baseAuth = {
headers: [{ key: 'Authorization', value: 'Bearer token' }],
};

const res = await supertest
.put(`/api/fleet/agent_download_sources/${createRes.item.id}`)
.set('kbn-xsrf', 'xxxx')
.send({
name: `Download source auth conflict ${Date.now()}`,
host: 'http://test.fr:443',
is_default: false,
auth: { ...baseAuth, ...auth },
...(secrets ? { secrets } : {}),
})
.expect(400);

expect(res.body.message).to.contain('Cannot use "Authorization" custom header');
});
});

it('should delete password secret when switching from username/password to api_key', async function () {
await clearAgents();
await createFleetServerAgent(fleetServerPolicyId, 'server_1', '9.4.0');
Expand Down
Loading