[Fleet] Create a fleet_enroll user and role during fleet setup (#60562)
This commit is contained in:
parent
103f217964
commit
558aaaa8ba
|
@ -15,8 +15,8 @@ export interface NewOutput {
|
|||
hosts?: string[];
|
||||
ca_sha256?: string;
|
||||
api_key?: string;
|
||||
admin_username?: string;
|
||||
admin_password?: string;
|
||||
fleet_enroll_username?: string;
|
||||
fleet_enroll_password?: string;
|
||||
config?: Record<string, any>;
|
||||
}
|
||||
|
||||
|
|
|
@ -9,8 +9,8 @@ export interface GetFleetSetupRequest {}
|
|||
|
||||
export interface CreateFleetSetupRequest {
|
||||
body: {
|
||||
admin_username: string;
|
||||
admin_password: string;
|
||||
fleet_enroll_username: string;
|
||||
fleet_enroll_password: string;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -76,7 +76,7 @@ classDiagram
|
|||
ca_sha256
|
||||
config
|
||||
// Encrypted - user to create API keys
|
||||
admin_username
|
||||
admin_password
|
||||
fleet_enroll_username
|
||||
fleet_enroll_password
|
||||
}
|
||||
|
||||
|
|
|
@ -4,29 +4,26 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import React, { useState } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import {
|
||||
EuiPageBody,
|
||||
EuiPageContent,
|
||||
EuiForm,
|
||||
EuiFormRow,
|
||||
EuiFieldText,
|
||||
EuiFieldPassword,
|
||||
EuiText,
|
||||
EuiButton,
|
||||
EuiCallOut,
|
||||
EuiTitle,
|
||||
EuiSpacer,
|
||||
EuiIcon,
|
||||
} from '@elastic/eui';
|
||||
import { sendRequest, useInput, useCore } from '../../../hooks';
|
||||
import { sendRequest, useCore } from '../../../hooks';
|
||||
import { fleetSetupRouteService } from '../../../services';
|
||||
import { WithoutHeaderLayout } from '../../../layouts';
|
||||
|
||||
export const SetupPage: React.FunctionComponent<{
|
||||
refresh: () => Promise<void>;
|
||||
}> = ({ refresh }) => {
|
||||
const [isFormLoading, setIsFormLoading] = useState<boolean>(false);
|
||||
const core = useCore();
|
||||
const usernameInput = useInput();
|
||||
const passwordInput = useInput();
|
||||
|
||||
const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
@ -35,10 +32,6 @@ export const SetupPage: React.FunctionComponent<{
|
|||
await sendRequest({
|
||||
method: 'post',
|
||||
path: fleetSetupRouteService.postFleetSetupPath(),
|
||||
body: JSON.stringify({
|
||||
admin_username: usernameInput.value,
|
||||
admin_password: passwordInput.value,
|
||||
}),
|
||||
});
|
||||
await refresh();
|
||||
} catch (error) {
|
||||
|
@ -48,33 +41,47 @@ export const SetupPage: React.FunctionComponent<{
|
|||
};
|
||||
|
||||
return (
|
||||
<EuiPageBody>
|
||||
<EuiPageContent>
|
||||
<EuiTitle>
|
||||
<h1>Setup</h1>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="l" />
|
||||
<EuiCallOut title="Warning!" color="warning" iconType="help">
|
||||
<EuiText>
|
||||
To setup fleet and ingest you need to a enable a user that can create API Keys and write
|
||||
to logs-* and metrics-*
|
||||
<WithoutHeaderLayout>
|
||||
<EuiPageBody restrictWidth={528}>
|
||||
<EuiPageContent
|
||||
verticalPosition="center"
|
||||
horizontalPosition="center"
|
||||
className="eui-textCenter"
|
||||
paddingSize="l"
|
||||
>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiIcon type="lock" color="subdued" size="xl" />
|
||||
<EuiSpacer size="m" />
|
||||
<EuiTitle size="l">
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="xpack.ingestManager.setupPage.title"
|
||||
defaultMessage="Enable Fleet"
|
||||
/>
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="xl" />
|
||||
<EuiText color="subdued">
|
||||
<FormattedMessage
|
||||
id="xpack.ingestManager.setupPage.description"
|
||||
defaultMessage="In order to use Fleet, you must create an Elastic user. This user can create API keys
|
||||
and write to logs-* and metrics-*."
|
||||
/>
|
||||
</EuiText>
|
||||
</EuiCallOut>
|
||||
<EuiSpacer size="l" />
|
||||
<EuiForm>
|
||||
<form onSubmit={onSubmit}>
|
||||
<EuiFormRow label="Username">
|
||||
<EuiFieldText name="username" {...usernameInput.props} />
|
||||
</EuiFormRow>
|
||||
<EuiFormRow label="Password">
|
||||
<EuiFieldPassword name="password" {...passwordInput.props} />
|
||||
</EuiFormRow>
|
||||
<EuiButton isLoading={isFormLoading} type="submit">
|
||||
Submit
|
||||
</EuiButton>
|
||||
</form>
|
||||
</EuiForm>
|
||||
</EuiPageContent>
|
||||
</EuiPageBody>
|
||||
<EuiSpacer size="l" />
|
||||
<EuiForm>
|
||||
<form onSubmit={onSubmit}>
|
||||
<EuiButton fill isLoading={isFormLoading} type="submit">
|
||||
<FormattedMessage
|
||||
id="xpack.ingestManager.setupPage.enableFleet"
|
||||
defaultMessage="Create user and enable Fleet"
|
||||
/>
|
||||
</EuiButton>
|
||||
</form>
|
||||
</EuiForm>
|
||||
<EuiSpacer size="m" />
|
||||
</EuiPageContent>
|
||||
</EuiPageBody>
|
||||
</WithoutHeaderLayout>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -3,12 +3,10 @@
|
|||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { TypeOf } from '@kbn/config-schema';
|
||||
import { RequestHandler } from 'src/core/server';
|
||||
import { outputService, agentConfigService } from '../../services';
|
||||
import { CreateFleetSetupRequestSchema, CreateFleetSetupResponse } from '../../types';
|
||||
import { setup } from '../../services/setup';
|
||||
import { generateEnrollmentAPIKey } from '../../services/api_keys';
|
||||
import { outputService } from '../../services';
|
||||
import { CreateFleetSetupResponse } from '../../types';
|
||||
import { setupIngestManager, setupFleet } from '../../services/setup';
|
||||
|
||||
export const getFleetSetupHandler: RequestHandler = async (context, request, response) => {
|
||||
const soClient = context.core.savedObjects.client;
|
||||
|
@ -32,21 +30,12 @@ export const getFleetSetupHandler: RequestHandler = async (context, request, res
|
|||
}
|
||||
};
|
||||
|
||||
export const createFleetSetupHandler: RequestHandler<
|
||||
undefined,
|
||||
undefined,
|
||||
TypeOf<typeof CreateFleetSetupRequestSchema.body>
|
||||
> = async (context, request, response) => {
|
||||
const soClient = context.core.savedObjects.client;
|
||||
export const createFleetSetupHandler: RequestHandler = async (context, request, response) => {
|
||||
try {
|
||||
await outputService.updateOutput(soClient, await outputService.getDefaultOutputId(soClient), {
|
||||
admin_username: request.body.admin_username,
|
||||
admin_password: request.body.admin_password,
|
||||
});
|
||||
await generateEnrollmentAPIKey(soClient, {
|
||||
name: 'Default',
|
||||
configId: await agentConfigService.getDefaultAgentConfigId(soClient),
|
||||
});
|
||||
const soClient = context.core.savedObjects.client;
|
||||
const callCluster = context.core.elasticsearch.adminClient.callAsCurrentUser;
|
||||
await setupIngestManager(soClient, callCluster);
|
||||
await setupFleet(soClient, callCluster);
|
||||
|
||||
return response.ok({
|
||||
body: { isInitialized: true },
|
||||
|
@ -63,7 +52,7 @@ export const ingestManagerSetupHandler: RequestHandler = async (context, request
|
|||
const soClient = context.core.savedObjects.client;
|
||||
const callCluster = context.core.elasticsearch.adminClient.callAsCurrentUser;
|
||||
try {
|
||||
await setup(soClient, callCluster);
|
||||
await setupIngestManager(soClient, callCluster);
|
||||
return response.ok({
|
||||
body: { isInitialized: true },
|
||||
});
|
||||
|
|
|
@ -102,8 +102,8 @@ export const savedObjectMappings = {
|
|||
ca_sha256: { type: 'keyword' },
|
||||
// FIXME_INGEST https://github.com/elastic/kibana/issues/56554
|
||||
api_key: { type: 'keyword' },
|
||||
admin_username: { type: 'binary' },
|
||||
admin_password: { type: 'binary' },
|
||||
fleet_enroll_username: { type: 'binary' },
|
||||
fleet_enroll_password: { type: 'binary' },
|
||||
config: { type: 'flattened' },
|
||||
},
|
||||
},
|
||||
|
|
|
@ -60,13 +60,13 @@ class OutputService {
|
|||
.getEncryptedSavedObjects()
|
||||
?.getDecryptedAsInternalUser<Output>(OUTPUT_SAVED_OBJECT_TYPE, defaultOutputId);
|
||||
|
||||
if (!so || !so.attributes.admin_username || !so.attributes.admin_password) {
|
||||
if (!so || !so.attributes.fleet_enroll_username || !so.attributes.fleet_enroll_password) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
username: so!.attributes.admin_username,
|
||||
password: so!.attributes.admin_password,
|
||||
username: so!.attributes.fleet_enroll_username,
|
||||
password: so!.attributes.fleet_enroll_password,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import uuid from 'uuid';
|
||||
import { SavedObjectsClientContract } from 'src/core/server';
|
||||
import { CallESAsCurrentUser } from '../types';
|
||||
import { agentConfigService } from './agent_config';
|
||||
|
@ -19,8 +20,12 @@ import {
|
|||
} from '../../common';
|
||||
import { getPackageInfo } from './epm/packages';
|
||||
import { datasourceService } from './datasource';
|
||||
import { generateEnrollmentAPIKey } from './api_keys';
|
||||
|
||||
export async function setup(
|
||||
const FLEET_ENROLL_USERNAME = 'fleet_enroll';
|
||||
const FLEET_ENROLL_ROLE = 'fleet_enroll';
|
||||
|
||||
export async function setupIngestManager(
|
||||
soClient: SavedObjectsClientContract,
|
||||
callCluster: CallESAsCurrentUser
|
||||
) {
|
||||
|
@ -60,6 +65,53 @@ export async function setup(
|
|||
}
|
||||
}
|
||||
|
||||
export async function setupFleet(
|
||||
soClient: SavedObjectsClientContract,
|
||||
callCluster: CallESAsCurrentUser
|
||||
) {
|
||||
// Create fleet_enroll role
|
||||
// This should be done directly in ES at some point
|
||||
await callCluster('transport.request', {
|
||||
method: 'PUT',
|
||||
path: `/_security/role/${FLEET_ENROLL_ROLE}`,
|
||||
body: {
|
||||
cluster: ['monitor', 'manage_api_key'],
|
||||
indices: [
|
||||
{
|
||||
names: ['logs-*', 'metrics-*', 'events-*'],
|
||||
privileges: ['write', 'create_index'],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
const password = generateRandomPassword();
|
||||
// Create fleet enroll user
|
||||
await callCluster('transport.request', {
|
||||
method: 'PUT',
|
||||
path: `/_security/user/${FLEET_ENROLL_USERNAME}`,
|
||||
body: {
|
||||
password,
|
||||
roles: [FLEET_ENROLL_ROLE],
|
||||
},
|
||||
});
|
||||
|
||||
// save fleet admin user
|
||||
await outputService.updateOutput(soClient, await outputService.getDefaultOutputId(soClient), {
|
||||
fleet_enroll_username: FLEET_ENROLL_USERNAME,
|
||||
fleet_enroll_password: password,
|
||||
});
|
||||
|
||||
// Generate default enrollment key
|
||||
await generateEnrollmentAPIKey(soClient, {
|
||||
name: 'Default',
|
||||
configId: await agentConfigService.getDefaultAgentConfigId(soClient),
|
||||
});
|
||||
}
|
||||
|
||||
function generateRandomPassword() {
|
||||
return Buffer.from(uuid.v4()).toString('base64');
|
||||
}
|
||||
|
||||
async function addPackageToConfig(
|
||||
soClient: SavedObjectsClientContract,
|
||||
packageToInstall: Installation,
|
||||
|
|
|
@ -15,8 +15,8 @@ const OutputBaseSchema = {
|
|||
type: schema.oneOf([schema.literal(OutputType.Elasticsearch)]),
|
||||
hosts: schema.maybe(schema.arrayOf(schema.string())),
|
||||
api_key: schema.maybe(schema.string()),
|
||||
admin_username: schema.maybe(schema.string()),
|
||||
admin_password: schema.maybe(schema.string()),
|
||||
fleet_enroll_username: schema.maybe(schema.string()),
|
||||
fleet_enroll_password: schema.maybe(schema.string()),
|
||||
config: schema.maybe(schema.recordOf(schema.string(), schema.any())),
|
||||
};
|
||||
|
||||
|
|
|
@ -3,16 +3,10 @@
|
|||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { schema } from '@kbn/config-schema';
|
||||
|
||||
export const GetFleetSetupRequestSchema = {};
|
||||
|
||||
export const CreateFleetSetupRequestSchema = {
|
||||
body: schema.object({
|
||||
admin_username: schema.string(),
|
||||
admin_password: schema.string(),
|
||||
}),
|
||||
};
|
||||
export const CreateFleetSetupRequestSchema = {};
|
||||
|
||||
export interface CreateFleetSetupResponse {
|
||||
isInitialized: boolean;
|
||||
|
|
|
@ -39,10 +39,6 @@ export function setupIngest({ getService }: FtrProviderContext) {
|
|||
.send();
|
||||
await getService('supertest')
|
||||
.post(`/api/ingest_manager/fleet/setup`)
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.send({
|
||||
admin_username: 'elastic',
|
||||
admin_password: 'changeme',
|
||||
});
|
||||
.set('kbn-xsrf', 'xxx');
|
||||
});
|
||||
}
|
||||
|
|
|
@ -16,5 +16,6 @@ export default function loadTests({ loadTestFile }) {
|
|||
loadTestFile(require.resolve('./enrollment_api_keys/crud'));
|
||||
loadTestFile(require.resolve('./install'));
|
||||
loadTestFile(require.resolve('./agents/actions'));
|
||||
loadTestFile(require.resolve('./setup'));
|
||||
});
|
||||
}
|
||||
|
|
71
x-pack/test/api_integration/apis/fleet/setup.ts
Normal file
71
x-pack/test/api_integration/apis/fleet/setup.ts
Normal file
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
|
||||
export default function({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const es = getService('es');
|
||||
|
||||
describe('fleet_setup', () => {
|
||||
before(async () => {
|
||||
try {
|
||||
await es.security.deleteUser({
|
||||
username: 'fleet_enroll',
|
||||
});
|
||||
} catch (e) {
|
||||
if (e.meta?.statusCode !== 404) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
try {
|
||||
await es.security.deleteRole({
|
||||
name: 'fleet_enroll',
|
||||
});
|
||||
} catch (e) {
|
||||
if (e.meta?.statusCode !== 404) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('should create a fleet_enroll user and role', async () => {
|
||||
const { body: apiResponse } = await supertest
|
||||
.post(`/api/ingest_manager/fleet/setup`)
|
||||
.set('kbn-xsrf', 'xxxx')
|
||||
.expect(200);
|
||||
|
||||
expect(apiResponse.isInitialized).to.be(true);
|
||||
|
||||
const { body: userResponse } = await es.security.getUser({
|
||||
username: 'fleet_enroll',
|
||||
});
|
||||
|
||||
expect(userResponse).to.have.key('fleet_enroll');
|
||||
expect(userResponse.fleet_enroll.roles).to.eql(['fleet_enroll']);
|
||||
|
||||
const { body: roleResponse } = await es.security.getRole({
|
||||
name: 'fleet_enroll',
|
||||
});
|
||||
expect(roleResponse).to.have.key('fleet_enroll');
|
||||
expect(roleResponse.fleet_enroll).to.eql({
|
||||
cluster: ['monitor', 'manage_api_key'],
|
||||
indices: [
|
||||
{
|
||||
names: ['logs-*', 'metrics-*', 'events-*'],
|
||||
privileges: ['write', 'create_index'],
|
||||
allow_restricted_indices: false,
|
||||
},
|
||||
],
|
||||
applications: [],
|
||||
run_as: [],
|
||||
metadata: {},
|
||||
transient_metadata: { enabled: true },
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
Loading…
Reference in a new issue