[Fleet] Create a fleet_enroll user and role during fleet setup (#60562)

This commit is contained in:
Nicolas Chaulet 2020-03-20 13:31:56 -04:00 committed by GitHub
parent 103f217964
commit 558aaaa8ba
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 194 additions and 84 deletions

View file

@ -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>;
}

View file

@ -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;
};
}

View file

@ -76,7 +76,7 @@ classDiagram
ca_sha256
config
// Encrypted - user to create API keys
admin_username
admin_password
fleet_enroll_username
fleet_enroll_password
}

View file

@ -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>
);
};

View file

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

View file

@ -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' },
},
},

View file

@ -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,
};
}

View file

@ -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,

View file

@ -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())),
};

View file

@ -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;

View file

@ -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');
});
}

View file

@ -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'));
});
}

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