Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Fleet] Handle missing permissions when creating standalone agent API keys #193218

Merged
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 @@ -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);
Copy link
Member Author

Choose a reason for hiding this comment

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

I added a loading state, as it was weird to me to not have feedback while the API key is creating

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>({
Copy link
Member Author

Choose a reason for hiding this comment

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

use sendRequestForRq as it throws on error, sendRequest return an error that was not handled

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 },
Copy link
Member Author

Choose a reason for hiding this comment

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

we should only have the addAgents permission and not all

},
})
.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;
}
}
Loading