Skip to content

Commit

Permalink
[Fleet] Handle missing permissions when creating standalone agent API…
Browse files Browse the repository at this point in the history
… keys (elastic#193218)

(cherry picked from commit 6a79e2d)
  • Loading branch information
nchaulet committed Sep 18, 2024
1 parent c8043c9 commit aa14ae9
Show file tree
Hide file tree
Showing 11 changed files with 131 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ export const InstallElasticAgentStandalonePageStep: React.FC<InstallAgentPagePro
const [commandCopied, setCommandCopied] = useState(false);
const [policyCopied, setPolicyCopied] = useState(false);

const { yaml, onCreateApiKey, apiKey, downloadYaml } = useFetchFullPolicy(agentPolicy);
const { yaml, onCreateApiKey, isCreatingApiKey, apiKey, downloadYaml } =
useFetchFullPolicy(agentPolicy);

if (!agentPolicy) {
return (
Expand All @@ -60,6 +61,7 @@ export const InstallElasticAgentStandalonePageStep: React.FC<InstallAgentPagePro
downloadYaml,
apiKey,
onCreateApiKey,
isCreatingApiKey,
isComplete: policyCopied,
onCopy: () => setPolicyCopied(true),
}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -210,12 +210,15 @@ export function useGetCreateApiKey() {
const core = useStartServices();

const [apiKey, setApiKey] = useState<string | undefined>(undefined);
const [isLoading, setIsLoading] = useState(false);
const onCreateApiKey = useCallback(async () => {
try {
setIsLoading(true);
const res = await sendCreateStandaloneAgentAPIKey({
name: crypto.randomBytes(16).toString('hex'),
});
const newApiKey = `${res.data?.item.id}:${res.data?.item.api_key}`;

const newApiKey = `${res.item.id}:${res.item.api_key}`;
setApiKey(newApiKey);
} catch (err) {
core.notifications.toasts.addError(err, {
Expand All @@ -224,9 +227,11 @@ export function useGetCreateApiKey() {
}),
});
}
setIsLoading(false);
}, [core.notifications.toasts]);
return {
apiKey,
isLoading,
onCreateApiKey,
};
}
Expand All @@ -235,7 +240,7 @@ export function useFetchFullPolicy(agentPolicy: AgentPolicy | undefined, isK8s?:
const core = useStartServices();
const [yaml, setYaml] = useState<any | undefined>('');
const [fullAgentPolicy, setFullAgentPolicy] = useState<FullAgentPolicy | undefined>();
const { apiKey, onCreateApiKey } = useGetCreateApiKey();
const { apiKey, isLoading: isCreatingApiKey, onCreateApiKey } = useGetCreateApiKey();

useEffect(() => {
async function fetchFullPolicy() {
Expand Down Expand Up @@ -302,6 +307,7 @@ export function useFetchFullPolicy(agentPolicy: AgentPolicy | undefined, isK8s?:
yaml,
onCreateApiKey,
fullAgentPolicy,
isCreatingApiKey,
apiKey,
downloadYaml,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,10 @@ export const StandaloneSteps: React.FunctionComponent<InstructionProps> = ({
isK8s,
cloudSecurityIntegration,
}) => {
const { yaml, onCreateApiKey, apiKey, downloadYaml } = useFetchFullPolicy(selectedPolicy, isK8s);
const { yaml, onCreateApiKey, isCreatingApiKey, apiKey, downloadYaml } = useFetchFullPolicy(
selectedPolicy,
isK8s
);

const agentVersion = useAgentVersion();

Expand Down Expand Up @@ -88,6 +91,7 @@ export const StandaloneSteps: React.FunctionComponent<InstructionProps> = ({
downloadYaml,
apiKey,
onCreateApiKey,
isCreatingApiKey,
})
);

Expand Down Expand Up @@ -116,6 +120,7 @@ export const StandaloneSteps: React.FunctionComponent<InstructionProps> = ({
downloadYaml,
apiKey,
onCreateApiKey,
isCreatingApiKey,
cloudSecurityIntegration,
mode,
setMode,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export const ConfigureStandaloneAgentStep = ({
downloadYaml,
apiKey,
onCreateApiKey,
isCreatingApiKey,
isComplete,
onCopy,
}: {
Expand All @@ -43,6 +44,7 @@ export const ConfigureStandaloneAgentStep = ({
downloadYaml: () => void;
apiKey: string | undefined;
onCreateApiKey: () => void;
isCreatingApiKey: boolean;
isComplete?: boolean;
onCopy?: () => void;
}): EuiContainedStepProps => {
Expand Down Expand Up @@ -167,7 +169,7 @@ export const ConfigureStandaloneAgentStep = ({
<EuiSpacer size="s" />
<EuiFlexGroup gutterSize="m">
<EuiFlexItem grow={false}>
<EuiButton onClick={onCreateApiKey}>
<EuiButton onClick={onCreateApiKey} isLoading={isCreatingApiKey}>
<FormattedMessage
id="xpack.fleet.agentEnrollment.createApiKeyButton"
defaultMessage="Create API key"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ import type {

import { API_VERSIONS, CREATE_STANDALONE_AGENT_API_KEY_ROUTE } from '../../../common/constants';

import { sendRequest } from './use_request';
import { sendRequestForRq } from './use_request';

export function sendCreateStandaloneAgentAPIKey(body: PostStandaloneAgentAPIKeyRequest['body']) {
return sendRequest<PostStandaloneAgentAPIKeyResponse>({
return sendRequestForRq<PostStandaloneAgentAPIKeyResponse>({
method: 'post',
path: CREATE_STANDALONE_AGENT_API_KEY_ROUTE,
version: API_VERSIONS.internal.v1,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,38 @@ import type { TypeOf } from '@kbn/config-schema';

import { createStandaloneAgentApiKey } from '../../services/api_keys';
import type { FleetRequestHandler, PostStandaloneAgentAPIKeyRequestSchema } from '../../types';
import {
INDEX_PRIVILEGES,
canCreateStandaloneAgentApiKey,
} from '../../services/api_keys/create_standalone_agent_api_key';
import { FleetUnauthorizedError, defaultFleetErrorHandler } from '../../errors';

export const createStandaloneAgentApiKeyHandler: FleetRequestHandler<
undefined,
undefined,
TypeOf<typeof PostStandaloneAgentAPIKeyRequestSchema.body>
> = async (context, request, response) => {
const coreContext = await context.core;
const esClient = coreContext.elasticsearch.client.asCurrentUser;
const key = await createStandaloneAgentApiKey(esClient, request.body.name);
return response.ok({
body: {
item: key,
},
});
try {
const coreContext = await context.core;
const esClient = coreContext.elasticsearch.client.asCurrentUser;
const canCreate = await canCreateStandaloneAgentApiKey(esClient);

if (!canCreate) {
throw new FleetUnauthorizedError(
`Missing permissions to create standalone API key, You need ${INDEX_PRIVILEGES.privileges.join(
', '
)} for indices ${INDEX_PRIVILEGES.names.join(', ')}`
);
}

const key = await createStandaloneAgentApiKey(esClient, request.body.name);

return response.ok({
body: {
item: key,
},
});
} catch (error) {
return defaultFleetErrorHandler({ error, response });
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export const registerRoutes = (router: FleetAuthzRouter) => {
path: CREATE_STANDALONE_AGENT_API_KEY_ROUTE,
access: 'internal',
fleetAuthz: {
fleet: { all: true },
fleet: { addAgents: true },
},
})
.addVersion(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,21 @@

import type { ElasticsearchClient } from '@kbn/core/server';

export const CLUSTER_PRIVILEGES = ['monitor'];

export const INDEX_PRIVILEGES = {
names: ['logs-*-*', 'metrics-*-*', 'traces-*-*', 'synthetics-*-*'],
privileges: ['auto_configure', 'create_doc'],
};

export async function canCreateStandaloneAgentApiKey(esClient: ElasticsearchClient) {
const res = await esClient.security.hasPrivileges({
cluster: CLUSTER_PRIVILEGES,
index: [INDEX_PRIVILEGES],
});

return res.has_all_requested;
}
export function createStandaloneAgentApiKey(esClient: ElasticsearchClient, name: string) {
// Based on https://www.elastic.co/guide/en/fleet/master/grant-access-to-elasticsearch.html#create-api-key-standalone-agent
return esClient.security.createApiKey({
Expand All @@ -17,13 +32,8 @@ export function createStandaloneAgentApiKey(esClient: ElasticsearchClient, name:
},
role_descriptors: {
standalone_agent: {
cluster: ['monitor'],
indices: [
{
names: ['logs-*-*', 'metrics-*-*', 'traces-*-*', 'synthetics-*-*'],
privileges: ['auto_configure', 'create_doc'],
},
],
cluster: CLUSTER_PRIVILEGES,
indices: [INDEX_PRIVILEGES],
},
},
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* 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 expect from '@kbn/expect';

import { FtrProviderContext } from '../../../api_integration/ftr_provider_context';
import { skipIfNoDockerRegistry } from '../../helpers';
import { SpaceTestApiClient } from '../space_awareness/api_helper';
import { expectToRejectWithError } from '../space_awareness/helpers';
import { setupTestUsers, testUsers } from '../test_users';

export default function (providerContext: FtrProviderContext) {
const { getService } = providerContext;

const supertestWithoutAuth = getService('supertestWithoutAuth');
const supertest = getService('supertest');

describe('create standalone api key', function () {
skipIfNoDockerRegistry(providerContext);

before(async () => {
await setupTestUsers(getService('security'));
});

describe('POST /internal/fleet/create_standalone_agent_api_key', () => {
it('should work with a user with the correct permissions', async () => {
const apiClient = new SpaceTestApiClient(supertest);
const res = await apiClient.postStandaloneApiKey('test');
expect(res.item.name).to.eql('standalone_agent-test');
});
it('should return a 403 if the user cannot create the api key', async () => {
const apiClient = new SpaceTestApiClient(supertestWithoutAuth, {
username: testUsers.fleet_all_int_all.username,
password: testUsers.fleet_all_int_all.password,
});
await expectToRejectWithError(
() => apiClient.postStandaloneApiKey('tata'),
/403 Forbidden Missing permissions to create standalone API key/
);
});
});
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@ export default function loadTests({ loadTestFile }) {
loadTestFile(require.resolve('./agent_policy_datastream_permissions'));
loadTestFile(require.resolve('./privileges'));
loadTestFile(require.resolve('./agent_policy_root_integrations'));
loadTestFile(require.resolve('./create_standalone_api_key'));
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -554,4 +554,19 @@ export class SpaceTestApiClient {

return res;
}

async postStandaloneApiKey(name: string, spaceId?: string) {
const { body: res, statusCode } = await this.supertest
.post(`${this.getBaseUrl(spaceId)}/internal/fleet/create_standalone_agent_api_key`)
.auth(this.auth.username, this.auth.password)
.set('kbn-xsrf', 'xxxx')
.set('elastic-api-version', '1')
.send({ name });

if (statusCode !== 200) {
throw new Error(`${statusCode} ${res?.error} ${res.message}`);
}

return res;
}
}

0 comments on commit aa14ae9

Please sign in to comment.