mirror of
https://github.com/hashicorp/vault.git
synced 2025-11-18 09:11:27 +01:00
* adds api service * adds missing copyright headers * Update ui/app/services/api.ts Co-authored-by: claire bontempo <68122737+hellobontempo@users.noreply.github.com> * removes response cache and comments from api service * removes hide warnings condition from showWarnings middleware in api service * splits out setHeaders test --------- Co-authored-by: claire bontempo <68122737+hellobontempo@users.noreply.github.com>
176 lines
5.9 KiB
TypeScript
176 lines
5.9 KiB
TypeScript
/**
|
|
* Copyright (c) HashiCorp, Inc.
|
|
* SPDX-License-Identifier: BUSL-1.1
|
|
*/
|
|
|
|
import Service, { service } from '@ember/service';
|
|
import {
|
|
Configuration,
|
|
RequestContext,
|
|
ResponseContext,
|
|
AuthApi,
|
|
IdentityApi,
|
|
SecretsApi,
|
|
SystemApi,
|
|
} from '@hashicorp/vault-client-typescript';
|
|
import config from '../config/environment';
|
|
|
|
import type AuthService from 'vault/services/auth';
|
|
import type NamespaceService from 'vault/services/namespace';
|
|
import type ControlGroupService from 'vault/services/control-group';
|
|
import type FlashMessageService from 'vault/services/flash-messages';
|
|
import type { ApiError, HeaderMap, XVaultHeaders } from 'vault/api';
|
|
|
|
export default class ApiService extends Service {
|
|
@service('auth') declare readonly authService: AuthService;
|
|
@service('namespace') declare readonly namespaceService: NamespaceService;
|
|
@service declare readonly controlGroup: ControlGroupService;
|
|
@service declare readonly flashMessages: FlashMessageService;
|
|
|
|
// -- Pre Request Middleware --
|
|
setLastFetch = async (context: RequestContext) => {
|
|
const { url } = context;
|
|
const isPolling = config.APP.POLLING_URLS.some((str) => url.includes(str));
|
|
|
|
if (!isPolling) {
|
|
this.authService.setLastFetch(Date.now());
|
|
}
|
|
};
|
|
|
|
getControlGroupToken = async (context: RequestContext) => {
|
|
const { url, init } = context;
|
|
const controlGroupToken = this.controlGroup.tokenForUrl(url);
|
|
let newUrl = url;
|
|
// if we have a Control Group token that matches the url,
|
|
// unwrap it and return the unwrapped response as if it were the initial request
|
|
// to do this, we rewrite the request
|
|
if (controlGroupToken) {
|
|
const { token } = controlGroupToken;
|
|
const { headers } = this.buildHeaders({ token });
|
|
newUrl = '/v1/sys/wrapping/unwrap';
|
|
init.method = 'POST';
|
|
init.headers = headers;
|
|
init.body = JSON.stringify({ token });
|
|
}
|
|
return { url: newUrl, init };
|
|
};
|
|
|
|
setHeaders = async (context: RequestContext) => {
|
|
const { url, init } = context;
|
|
const headers = new Headers(init.headers);
|
|
// unauthenticated or clientToken requests should set the header in initOverrides
|
|
// unauthenticated value should be empty string, not undefined or null
|
|
if (!headers.has('X-Vault-Token')) {
|
|
headers.set('X-Vault-Token', this.authService.currentToken);
|
|
}
|
|
if (init.method === 'PATCH') {
|
|
headers.set('Content-Type', 'application/merge-patch+json');
|
|
}
|
|
// use initOverrides to set the namespace header to something other than path set in the namespace service
|
|
// for requests that must be made to root namespace pass empty string as value
|
|
const namespace = this.namespaceService.path;
|
|
if (!headers.has('X-Vault-Namespace') && namespace) {
|
|
headers.set('X-Vault-Namespace', namespace);
|
|
}
|
|
|
|
init.headers = headers;
|
|
return { url, init };
|
|
};
|
|
|
|
// -- Post Request Middleware --
|
|
showWarnings = async (context: ResponseContext) => {
|
|
const response = context.response.clone();
|
|
const json = await response?.json();
|
|
|
|
if (json?.warnings) {
|
|
json.warnings.forEach((message: string) => {
|
|
this.flashMessages.info(message);
|
|
});
|
|
}
|
|
};
|
|
|
|
deleteControlGroupToken = async (context: ResponseContext) => {
|
|
const { url } = context;
|
|
const controlGroupToken = this.controlGroup.tokenForUrl(url);
|
|
if (controlGroupToken) {
|
|
this.controlGroup.deleteControlGroupToken(controlGroupToken.accessor);
|
|
}
|
|
};
|
|
|
|
formatErrorResponse = async (context: ResponseContext) => {
|
|
const response = context.response.clone();
|
|
const { headers, status, statusText } = response;
|
|
|
|
// backwards compatibility with Ember Data
|
|
if (status >= 400) {
|
|
const error: ApiError = (await response?.json()) || {};
|
|
error.httpStatus = response?.status;
|
|
error.path = context.url;
|
|
// typically the Vault API error response looks like { errors: ['some error message'] }
|
|
// but sometimes (eg RespondWithStatusCode) it's { data: { error: 'some error message' } }
|
|
if (error?.data?.error && !error.errors) {
|
|
// normalize the errors from RespondWithStatusCode
|
|
error.errors = [error.data.error];
|
|
}
|
|
return new Response(JSON.stringify(error), { headers, status, statusText });
|
|
}
|
|
|
|
return;
|
|
};
|
|
|
|
// the responses in the OpenAPI spec don't account for the return values to be under the 'data' key
|
|
// return the data rather than the entire response
|
|
// if there is no data then return the full response
|
|
extractData = async (context: ResponseContext) => {
|
|
const response = context.response.clone();
|
|
const { headers, ok, status, statusText } = response;
|
|
|
|
if (ok) {
|
|
const json = await response?.json();
|
|
if (json.data) {
|
|
return new Response(JSON.stringify(json.data), { headers, status, statusText });
|
|
}
|
|
}
|
|
|
|
return;
|
|
};
|
|
// --- End Middleware ---
|
|
|
|
configuration = new Configuration({
|
|
basePath: '/v1',
|
|
middleware: [
|
|
{ pre: this.setLastFetch },
|
|
{ pre: this.getControlGroupToken },
|
|
{ pre: this.setHeaders },
|
|
{ post: this.showWarnings },
|
|
{ post: this.deleteControlGroupToken },
|
|
{ post: this.formatErrorResponse },
|
|
{ post: this.extractData },
|
|
],
|
|
});
|
|
|
|
auth = new AuthApi(this.configuration);
|
|
identity = new IdentityApi(this.configuration);
|
|
secrets = new SecretsApi(this.configuration);
|
|
sys = new SystemApi(this.configuration);
|
|
|
|
// convenience method for overriding headers for given requests to ensure consistency
|
|
// eg. this.api.sys.wrap(data, { headers: { 'X-Vault-Wrap-TTL': wrap } });
|
|
// -> this.api.sys.wrap(data, this.api.buildHeaders({ wrap }));
|
|
buildHeaders(headerMap: HeaderMap) {
|
|
const headers = {} as XVaultHeaders;
|
|
|
|
for (const key in headerMap) {
|
|
const headerKey = {
|
|
namespace: 'X-Vault-Namespace',
|
|
token: 'X-Vault-Token',
|
|
wrap: 'X-Vault-Wrap-TTL',
|
|
}[key] as keyof XVaultHeaders;
|
|
|
|
headers[headerKey] = headerMap[key as keyof HeaderMap];
|
|
}
|
|
|
|
return { headers };
|
|
}
|
|
}
|