Skip to content

Commit

Permalink
[Fleet] Use fleet server indices for enrollment keys
Browse files Browse the repository at this point in the history
  • Loading branch information
nchaulet committed Dec 16, 2020
1 parent 744d680 commit 4a8791b
Show file tree
Hide file tree
Showing 12 changed files with 476 additions and 141 deletions.
2 changes: 2 additions & 0 deletions x-pack/plugins/fleet/common/constants/enrollment_api_key.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@
*/

export const ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE = 'fleet-enrollment-api-keys';

export const ENROLLMENT_API_KEYS_INDEX = '.fleet-enrollment-api-keys';
1 change: 1 addition & 0 deletions x-pack/plugins/fleet/common/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export interface FleetConfigType {
registryUrl?: string;
registryProxyUrl?: string;
agents: {
fleetServerEnabled: boolean;
enabled: boolean;
tlsCheckDisabled: boolean;
pollingRequestTimeout: number;
Expand Down
28 changes: 28 additions & 0 deletions x-pack/plugins/fleet/common/types/models/enrollment_api_key.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,31 @@ export interface EnrollmentAPIKey {
}

export type EnrollmentAPIKeySOAttributes = Omit<EnrollmentAPIKey, 'id'>;

// Generated

/**
* An Elastic Agent enrollment API key
*/
export interface FleetServerEnrollmentAPIKey {
/**
* True when the key is active
*/
active?: boolean;
/**
* The unique identifier for the enrollment key, currently xid
*/
api_key_id: string;
/**
* Api key
*/
api_key: string;
/**
* Enrollment key name
*/
name?: string;
policy_id?: string;
expire_at?: string;
created_at?: string;
updated_at?: string;
}
2 changes: 2 additions & 0 deletions x-pack/plugins/fleet/server/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ export {
INDEX_PATTERN_SAVED_OBJECT_TYPE,
ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE,
GLOBAL_SETTINGS_SAVED_OBJECT_TYPE,
// Fleet Server index
ENROLLMENT_API_KEYS_INDEX,
// Defaults
DEFAULT_AGENT_POLICY,
DEFAULT_OUTPUT,
Expand Down
1 change: 1 addition & 0 deletions x-pack/plugins/fleet/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export const config: PluginConfigDescriptor = {
registryProxyUrl: schema.maybe(schema.uri({ scheme: ['http', 'https'] })),
agents: schema.object({
enabled: schema.boolean({ defaultValue: true }),
fleetServerEnabled: schema.boolean({ defaultValue: false }),
tlsCheckDisabled: schema.boolean({ defaultValue: false }),
pollingRequestTimeout: schema.number({
defaultValue: AGENT_POLLING_REQUEST_TIMEOUT_MS,
Expand Down
29 changes: 21 additions & 8 deletions x-pack/plugins/fleet/server/routes/enrollment_api_key/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,18 @@ export const getEnrollmentApiKeysHandler: RequestHandler<
TypeOf<typeof GetEnrollmentAPIKeysRequestSchema.query>
> = async (context, request, response) => {
const soClient = context.core.savedObjects.client;
const esClient = context.core.elasticsearch.client.asCurrentUser;

try {
const { items, total, page, perPage } = await APIKeyService.listEnrollmentApiKeys(soClient, {
page: request.query.page,
perPage: request.query.perPage,
kuery: request.query.kuery,
});
const { items, total, page, perPage } = await APIKeyService.listEnrollmentApiKeys(
soClient,
esClient,
{
page: request.query.page,
perPage: request.query.perPage,
kuery: request.query.kuery,
}
);
const body: GetEnrollmentAPIKeysResponse = { list: items, total, page, perPage };

return response.ok({ body });
Expand All @@ -45,8 +51,9 @@ export const postEnrollmentApiKeyHandler: RequestHandler<
TypeOf<typeof PostEnrollmentAPIKeyRequestSchema.body>
> = async (context, request, response) => {
const soClient = context.core.savedObjects.client;
const esClient = context.core.elasticsearch.client.asCurrentUser;
try {
const apiKey = await APIKeyService.generateEnrollmentAPIKey(soClient, {
const apiKey = await APIKeyService.generateEnrollmentAPIKey(soClient, esClient, {
name: request.body.name,
expiration: request.body.expiration,
agentPolicyId: request.body.policy_id,
Expand All @@ -64,8 +71,9 @@ export const deleteEnrollmentApiKeyHandler: RequestHandler<
TypeOf<typeof DeleteEnrollmentAPIKeyRequestSchema.params>
> = async (context, request, response) => {
const soClient = context.core.savedObjects.client;
const esClient = context.core.elasticsearch.client.asCurrentUser;
try {
await APIKeyService.deleteEnrollmentApiKey(soClient, request.params.keyId);
await APIKeyService.deleteEnrollmentApiKey(soClient, esClient, request.params.keyId);

const body: DeleteEnrollmentAPIKeyResponse = { action: 'deleted' };

Expand All @@ -84,8 +92,13 @@ export const getOneEnrollmentApiKeyHandler: RequestHandler<
TypeOf<typeof GetOneEnrollmentAPIKeyRequestSchema.params>
> = async (context, request, response) => {
const soClient = context.core.savedObjects.client;
const esClient = context.core.elasticsearch.client.asCurrentUser;
try {
const apiKey = await APIKeyService.getEnrollmentAPIKey(soClient, request.params.keyId);
const apiKey = await APIKeyService.getEnrollmentAPIKey(
soClient,
esClient,
request.params.keyId
);
const body: GetOneEnrollmentAPIKeyResponse = { item: apiKey };

return response.ok({ body });
Expand Down
2 changes: 1 addition & 1 deletion x-pack/plugins/fleet/server/routes/setup/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export const createFleetSetupHandler: RequestHandler<
const soClient = context.core.savedObjects.client;
const callCluster = context.core.elasticsearch.legacy.client.callAsCurrentUser;
await setupIngestManager(soClient, callCluster);
await setupFleet(soClient, callCluster, {
await setupFleet(soClient, context.core.elasticsearch.client.asCurrentUser, callCluster, {
forceRecreate: request.body?.forceRecreate ?? false,
});

Expand Down
166 changes: 36 additions & 130 deletions x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,171 +4,77 @@
* you may not use this file except in compliance with the Elastic License.
*/

import uuid from 'uuid';
import Boom from '@hapi/boom';
import { SavedObjectsClientContract, SavedObject } from 'src/core/server';
import { EnrollmentAPIKey, EnrollmentAPIKeySOAttributes } from '../../types';
import { ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE } from '../../constants';
import { createAPIKey, invalidateAPIKey } from './security';
import { agentPolicyService } from '../agent_policy';
import { SavedObjectsClientContract, ElasticsearchClient } from 'src/core/server';
import { EnrollmentAPIKey } from '../../types';
import { appContextService } from '../app_context';
import { normalizeKuery } from '../saved_object';
import * as enrollmentApiKeyServiceSO from './enrollment_api_key_so';
import * as enrollmentApiKeyServiceFleetServer from './enrollment_api_key_fleet_server';

export async function listEnrollmentApiKeys(
soClient: SavedObjectsClientContract,
esClient: ElasticsearchClient,
options: {
page?: number;
perPage?: number;
kuery?: string;
showInactive?: boolean;
}
): Promise<{ items: EnrollmentAPIKey[]; total: any; page: any; perPage: any }> {
const { page = 1, perPage = 20, kuery } = options;

// eslint-disable-next-line @typescript-eslint/naming-convention
const { saved_objects, total } = await soClient.find<EnrollmentAPIKeySOAttributes>({
type: ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE,
page,
perPage,
sortField: 'created_at',
sortOrder: 'desc',
filter:
kuery && kuery !== ''
? normalizeKuery(ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, kuery)
: undefined,
});

const items = saved_objects.map(savedObjectToEnrollmentApiKey);

return {
items,
total,
page,
perPage,
};
if (appContextService.getConfig()?.agents?.fleetServerEnabled === true) {
return enrollmentApiKeyServiceFleetServer.listEnrollmentApiKeys(esClient, options);
} else {
return enrollmentApiKeyServiceSO.listEnrollmentApiKeys(soClient, options);
}
}

export async function getEnrollmentAPIKey(soClient: SavedObjectsClientContract, id: string) {
const so = await appContextService
.getEncryptedSavedObjects()
.getDecryptedAsInternalUser<EnrollmentAPIKeySOAttributes>(
ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE,
id
);
return savedObjectToEnrollmentApiKey(so);
export async function getEnrollmentAPIKey(
soClient: SavedObjectsClientContract,
esClient: ElasticsearchClient,
id: string
) {
if (appContextService.getConfig()?.agents?.fleetServerEnabled === true) {
return enrollmentApiKeyServiceFleetServer.getEnrollmentAPIKey(esClient, id);
} else {
return enrollmentApiKeyServiceSO.getEnrollmentAPIKey(soClient, id);
}
}

/**
* Invalidate an api key and mark it as inactive
* @param soClient
* @param id
*/
export async function deleteEnrollmentApiKey(soClient: SavedObjectsClientContract, id: string) {
const enrollmentApiKey = await getEnrollmentAPIKey(soClient, id);

await invalidateAPIKey(soClient, enrollmentApiKey.api_key_id);

await soClient.update(ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, id, {
active: false,
});
export async function deleteEnrollmentApiKey(
soClient: SavedObjectsClientContract,
esClient: ElasticsearchClient,
id: string
) {
if (appContextService.getConfig()?.agents?.fleetServerEnabled === true) {
return enrollmentApiKeyServiceFleetServer.deleteEnrollmentApiKey(soClient, esClient, id);
} else {
return enrollmentApiKeyServiceSO.deleteEnrollmentApiKey(soClient, id);
}
}

export async function deleteEnrollmentApiKeyForAgentPolicyId(
soClient: SavedObjectsClientContract,
agentPolicyId: string
) {
let hasMore = true;
let page = 1;
while (hasMore) {
const { items } = await listEnrollmentApiKeys(soClient, {
page: page++,
perPage: 100,
kuery: `${ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE}.policy_id:${agentPolicyId}`,
});

if (items.length === 0) {
hasMore = false;
}

for (const apiKey of items) {
await deleteEnrollmentApiKey(soClient, apiKey.id);
}
}
return enrollmentApiKeyServiceSO.deleteEnrollmentApiKeyForAgentPolicyId(soClient, agentPolicyId);
}

export async function generateEnrollmentAPIKey(
soClient: SavedObjectsClientContract,
esClient: ElasticsearchClient,
data: {
name?: string;
expiration?: string;
agentPolicyId?: string;
}
) {
const id = uuid.v4();
const { name: providedKeyName } = data;
if (data.agentPolicyId) {
await validateAgentPolicyId(soClient, data.agentPolicyId);
}
const agentPolicyId =
data.agentPolicyId ?? (await agentPolicyService.getDefaultAgentPolicyId(soClient));
const name = providedKeyName ? `${providedKeyName} (${id})` : id;
const key = await createAPIKey(soClient, name, {
// Useless role to avoid to have the privilege of the user that created the key
'fleet-apikey-enroll': {
cluster: [],
applications: [
{
application: '.fleet',
privileges: ['no-privileges'],
resources: ['*'],
},
],
},
});

if (!key) {
throw new Error('Unable to create an enrollment api key');
if (appContextService.getConfig()?.agents?.fleetServerEnabled === true) {
return enrollmentApiKeyServiceFleetServer.generateEnrollmentAPIKey(soClient, esClient, data);
} else {
return enrollmentApiKeyServiceSO.generateEnrollmentAPIKey(soClient, data);
}

const apiKey = Buffer.from(`${key.id}:${key.api_key}`).toString('base64');

const so = await soClient.create<EnrollmentAPIKeySOAttributes>(
ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE,
{
active: true,
api_key_id: key.id,
api_key: apiKey,
name,
policy_id: agentPolicyId,
created_at: new Date().toISOString(),
}
);

return getEnrollmentAPIKey(soClient, so.id);
}

async function validateAgentPolicyId(soClient: SavedObjectsClientContract, agentPolicyId: string) {
try {
await agentPolicyService.get(soClient, agentPolicyId);
} catch (e) {
if (e.isBoom && e.output.statusCode === 404) {
throw Boom.badRequest(`Agent policy ${agentPolicyId} does not exist`);
}
throw e;
}
}

function savedObjectToEnrollmentApiKey({
error,
attributes,
id,
}: SavedObject<EnrollmentAPIKeySOAttributes>): EnrollmentAPIKey {
if (error) {
throw new Error(error.message);
}

return {
id,
...attributes,
};
}
Loading

0 comments on commit 4a8791b

Please sign in to comment.