-
Notifications
You must be signed in to change notification settings - Fork 8.2k
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
[Upgrade Assistant] Add support for API keys when reindexing #111451
Changes from 7 commits
7f46f7e
6854680
8741bee
6d6b050
db9d5a6
c03bbb9
2d26060
e8b6f0f
1b2bd05
b005da7
01cca25
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,7 +8,10 @@ | |
import { createHash } from 'crypto'; | ||
import stringify from 'json-stable-stringify'; | ||
|
||
import { ReindexSavedObject } from '../../../common/types'; | ||
import { KibanaRequest, Logger } from 'src/core/server'; | ||
|
||
import { SecurityPluginStart } from '../../../../security/server'; | ||
import { ReindexSavedObject, ReindexStatus } from '../../../common/types'; | ||
|
||
export type Credential = Record<string, any>; | ||
|
||
|
@@ -20,25 +23,119 @@ export type Credential = Record<string, any>; | |
*/ | ||
export interface CredentialStore { | ||
get(reindexOp: ReindexSavedObject): Credential | undefined; | ||
set(reindexOp: ReindexSavedObject, credential: Credential): void; | ||
set(params: { | ||
reindexOp: ReindexSavedObject; | ||
request: KibanaRequest; | ||
security?: SecurityPluginStart; | ||
}): Promise<void>; | ||
update(params: { | ||
reindexOp: ReindexSavedObject; | ||
security?: SecurityPluginStart; | ||
credential: Credential; | ||
}): Promise<void>; | ||
clear(): void; | ||
} | ||
|
||
export const credentialStoreFactory = (): CredentialStore => { | ||
export const credentialStoreFactory = (logger: Logger): CredentialStore => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: I would find this code easier to digest if we extracted // Generates a stable hash for the reindex operation's current state.
const getHash = (reindexOp: ReindexSavedObject) =>
createHash('sha256')
.update(stringify({ id: reindexOp.id, ...reindexOp.attributes }))
.digest('base64');
const getApiKey = async ({
request,
security,
reindexOpId,
apiKeysMap,
}: {
request: KibanaRequest;
security?: SecurityPluginStart;
reindexOpId: string;
apiKeysMap: Map<string, string>;
}): Promise<string | undefined> => {
const apiKeyResult = await security?.authc.apiKeys.grantAsInternalUser(request, {
name: `ua_reindex_${reindexOpId}`,
role_descriptors: {},
});
if (apiKeyResult) {
const { api_key: apiKey, id } = apiKeyResult;
// Store each API key per reindex operation so that we can later invalidate it when the reindex operation is complete
apiKeysMap.set(reindexOpId, id);
// Returns the base64 encoding of `id:api_key`
// This can be used when sending a request with an "Authorization: ApiKey xxx" header
return Buffer.from(`${id}:${apiKey}`).toString('base64');
}
};
const invalidateApiKey = async ({
apiKeyId,
security,
log,
}: {
apiKeyId: string;
security?: SecurityPluginStart;
log: Logger;
}) => {
try {
await security?.authc.apiKeys.invalidateAsInternalUser({ ids: [apiKeyId] });
} catch (error) {
// Swallow error if there's a problem invalidating API key
log.debug(`Error invalidating API key for id ${apiKeyId}: ${error.message}`);
}
};
export const credentialStoreFactory = (logger: Logger): CredentialStore => {
/* ... */
}; |
||
const credMap = new Map<string, Credential>(); | ||
const apiKeysMap = new Map<string, string>(); | ||
const log = logger.get('credential_store'); | ||
|
||
// Generates a stable hash for the reindex operation's current state. | ||
const getHash = (reindexOp: ReindexSavedObject) => | ||
createHash('sha256') | ||
.update(stringify({ id: reindexOp.id, ...reindexOp.attributes })) | ||
.digest('base64'); | ||
|
||
const getApiKey = async ({ | ||
request, | ||
security, | ||
reindexOpId, | ||
}: { | ||
request: KibanaRequest; | ||
security?: SecurityPluginStart; | ||
reindexOpId: string; | ||
}): Promise<string | undefined> => { | ||
const apiKeyResult = await security?.authc.apiKeys.grantAsInternalUser(request, { | ||
name: `ua_reindex_${reindexOpId}`, | ||
role_descriptors: {}, | ||
}); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Optional nit: given the chance that an API key might not get deleted, maybe it's worth adding a bit of metadata? Something like this: diff --git a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/credential_store.ts b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/credential_store.ts
index 7f3f4f2b307..41eb7a5afa7 100644
--- a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/credential_store.ts
+++ b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/credential_store.ts
@@ -59,6 +59,10 @@ export const credentialStoreFactory = (logger: Logger): CredentialStore => {
const apiKeyResult = await security?.authc.apiKeys.grantAsInternalUser(request, {
name: `ua_reindex_${reindexOpId}`,
role_descriptors: {},
+ metadata: {
+ description:
+ 'Created by the Upgrade Assistant for a reindex operation; this can be safely deleted after Kibana is upgraded.',
+ },
});
if (apiKeyResult) { There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Great point! Done. |
||
|
||
if (apiKeyResult) { | ||
const { api_key: apiKey, id } = apiKeyResult; | ||
// Store each API key per reindex operation so that we can later invalidate it when the reindex operation is complete | ||
apiKeysMap.set(reindexOpId, id); | ||
// Returns the base64 encoding of `id:api_key` | ||
// This can be used when sending a request with an "Authorization: ApiKey xxx" header | ||
return Buffer.from(`${id}:${apiKey}`).toString('base64'); | ||
} | ||
}; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There are two edge cases that come to mind:
So I think that we probably need some additional logic to handle those edge cases, e.g., catch an error and fall back to just using the given credentials. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for the background! I've added a try/catch, which falls back to the user's credentials. |
||
|
||
const invalidateApiKey = async ({ | ||
apiKeyId, | ||
security, | ||
}: { | ||
apiKeyId: string; | ||
security?: SecurityPluginStart; | ||
}) => { | ||
try { | ||
await security?.authc.apiKeys.invalidateAsInternalUser({ ids: [apiKeyId] }); | ||
} catch (error) { | ||
// Swallow error if there's a problem invalidating API key | ||
log.debug(`Error invalidating API key for id ${apiKeyId}: ${error.message}`); | ||
} | ||
}; | ||
|
||
return { | ||
get(reindexOp: ReindexSavedObject) { | ||
return credMap.get(getHash(reindexOp)); | ||
}, | ||
|
||
set(reindexOp: ReindexSavedObject, credential: Credential) { | ||
async set({ | ||
reindexOp, | ||
request, | ||
security, | ||
}: { | ||
reindexOp: ReindexSavedObject; | ||
request: KibanaRequest; | ||
security?: SecurityPluginStart; | ||
}) { | ||
const areApiKeysEnabled = (await security?.authc.apiKeys.areAPIKeysEnabled()) ?? false; | ||
|
||
const apiKey = | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: we could simplify this a bit my moving logic into a condition that API keys are enabled: if (areApiKeysEnabled) {
const apiKey = await getApiKey({ request, security!, reindexOpId: reindexOp.id, apiKeysMap });
if (apiKey) {
credMap.set(getHash(reindexOp), {
...request.headers,
authorization: `ApiKey ${apiKey}`,
});
return;
}
}
// Set the requestor's credentials in memory if apiKeys are not enabled
credMap.set(getHash(reindexOp), request.headers); Once we're inside the condition we know that const getApiKey = async ({
request,
security,
reindexOpId,
apiKeysMap,
}: {
request: KibanaRequest;
security: SecurityPluginStart; // <-- No longer possibly undefined
reindexOpId: string;
apiKeysMap: Map<string, string>;
}): Promise<string | undefined> => {
/* ... */
}; |
||
areApiKeysEnabled && (await getApiKey({ request, security, reindexOpId: reindexOp.id })); | ||
|
||
if (apiKey) { | ||
credMap.set(getHash(reindexOp), { | ||
...request.headers, | ||
authorization: `ApiKey ${apiKey}`, | ||
}); | ||
return; | ||
} | ||
|
||
// Set the requestor's credentials in memory if apiKeys are not enabled | ||
credMap.set(getHash(reindexOp), request.headers); | ||
}, | ||
|
||
async update({ | ||
reindexOp, | ||
security, | ||
credential, | ||
}: { | ||
reindexOp: ReindexSavedObject; | ||
security?: SecurityPluginStart; | ||
credential: Credential; | ||
}) { | ||
// If the reindex operation is completed, and an API key is being used, invalidate it | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nittiest nit-pick ever: can we split this comment across two lines? As it reads, it sounds like line 138's comment means that we'll re-associate credentials if an API key isn't being used. // If the reindex operation is completed...
if (reindexOp.attributes.status === ReindexStatus.completed) {
// ...and an API key is being used, invalidate it
const apiKeyId = apiKeysMap.get(reindexOp.id);
if (apiKeyId) {
await invalidateApiKey({ apiKeyId, security, log });
return;
}
} |
||
if (reindexOp.attributes.status === ReindexStatus.completed) { | ||
const apiKeyId = apiKeysMap.get(reindexOp.id); | ||
if (apiKeyId) { | ||
await invalidateApiKey({ apiKeyId, security }); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we also want to delete the API key from apiKeysMap.delete(reindexOp.id); |
||
return; | ||
} | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The only downside of this is that it's an in-memory map, so if Kibana restarts for some reason before the reindex is finished, that API key won't ever get deleted. |
||
|
||
// Otherwise, re-associate the credentials | ||
credMap.set(getHash(reindexOp), credential); | ||
}, | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might be more succinct to express
not.toBeDefined()
astoBeUndefined()