From b789129f3d8373718e6ce613faebbe9503592cd5 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Thu, 10 Oct 2024 00:55:50 +0400 Subject: [PATCH 1/4] feat: less verbose errors --- src/custom/errors.ts | 52 ++++++++++++++++ src/custom/secrets.ts | 142 ++++++++++++++++++++++++------------------ 2 files changed, 134 insertions(+), 60 deletions(-) create mode 100644 src/custom/errors.ts diff --git a/src/custom/errors.ts b/src/custom/errors.ts new file mode 100644 index 0000000..9b75307 --- /dev/null +++ b/src/custom/errors.ts @@ -0,0 +1,52 @@ +import { AxiosError } from "axios"; + +type TApiErrorResponse = { + statusCode: number; + message: string; + error: string; +}; +export class InfisicalSDKError extends Error { + constructor(message: string) { + super(message); + this.message = message; + this.name = "InfisicalSDKError"; + } +} + +export class InfisicalSDKRequestError extends Error { + constructor( + message: string, + requestData: { + url: string; + method: string; + statusCode: number; + } + ) { + super(message); + this.message = `[URL=${requestData.url}] [Method=${requestData.method}] [StatusCode=${requestData.statusCode}] ${message}`; + this.name = "InfisicalSDKRequestError"; + } +} + +export const newInfisicalError = (error: any) => { + if (error instanceof AxiosError) { + const data = error?.response?.data as TApiErrorResponse; + + if (data?.message) { + return new InfisicalSDKRequestError(data.message, { + url: error.response?.config.url || "", + method: error.response?.config.method || "", + statusCode: error.response?.status || 0 + }); + } else if (error.message) { + return new InfisicalSDKError(error.message); + } else if (error.code) { + // If theres no message but a code is present, it's likely to be an aggregation error. This is not specific to Axios, but it falls under the AxiosError type + return new InfisicalSDKError(error.code); + } else { + return new InfisicalSDKError("Request failed with unknown error"); + } + } + + return new InfisicalSDKError(error?.message || "An error occurred"); +}; diff --git a/src/custom/secrets.ts b/src/custom/secrets.ts index 1b4b8a8..185754d 100644 --- a/src/custom/secrets.ts +++ b/src/custom/secrets.ts @@ -5,6 +5,7 @@ import type { DefaultApiApiV3SecretsRawSecretNamePatchRequest, DefaultApiApiV3SecretsRawSecretNamePostRequest } from "../infisicalapi_client"; +import { newInfisicalError } from "./errors"; type SecretType = "shared" | "personal"; @@ -48,80 +49,101 @@ export default class SecretsClient { #requestOptions: RawAxiosRequestConfig | undefined; constructor(apiInstance: InfisicalApi, requestOptions: RawAxiosRequestConfig | undefined) { this.#apiInstance = apiInstance; + this.#requestOptions = requestOptions; } listSecrets = async (options: ListSecretsOptions) => { - const res = await this.#apiInstance.apiV3SecretsRawGet( - { - environment: options.environment, - workspaceId: options.projectId, - expandSecretReferences: convertBool(options.expandSecretReferences), - includeImports: convertBool(options.includeImports), - recursive: convertBool(options.recursive), - secretPath: options.secretPath, - tagSlugs: options.tagSlugs ? options.tagSlugs.join(",") : undefined - }, - this.#requestOptions - ); - return res.data; + try { + const res = await this.#apiInstance.apiV3SecretsRawGet( + { + environment: options.environment, + workspaceId: options.projectId, + expandSecretReferences: convertBool(options.expandSecretReferences), + includeImports: convertBool(options.includeImports), + recursive: convertBool(options.recursive), + secretPath: options.secretPath, + tagSlugs: options.tagSlugs ? options.tagSlugs.join(",") : undefined + }, + this.#requestOptions + ); + return res.data; + } catch (err) { + throw newInfisicalError(err); + } }; getSecret = async (options: GetSecretOptions) => { - const res = await this.#apiInstance.apiV3SecretsRawSecretNameGet( - { - environment: options.environment, - secretName: options.secretName, - workspaceId: options.projectId, - expandSecretReferences: convertBool(options.expandSecretReferences), - includeImports: convertBool(options.includeImports), - secretPath: options.secretPath, - type: options.type, - version: options.version - }, - this.#requestOptions - ); - return res.data.secret; + try { + const res = await this.#apiInstance.apiV3SecretsRawSecretNameGet( + { + environment: options.environment, + secretName: options.secretName, + workspaceId: options.projectId, + expandSecretReferences: convertBool(options.expandSecretReferences), + includeImports: convertBool(options.includeImports), + secretPath: options.secretPath, + type: options.type, + version: options.version + }, + this.#requestOptions + ); + return res.data.secret; + } catch (err) { + throw newInfisicalError(err); + } }; updateSecret = async (secretName: DefaultApiApiV3SecretsRawSecretNamePatchRequest["secretName"], options: UpdateSecretOptions) => { - const res = await this.#apiInstance.apiV3SecretsRawSecretNamePatch( - { - secretName, - apiV3SecretsRawSecretNamePatchRequest: { - ...options, - workspaceId: options.projectId - } - }, - this.#requestOptions - ); - return res.data; + try { + const res = await this.#apiInstance.apiV3SecretsRawSecretNamePatch( + { + secretName, + apiV3SecretsRawSecretNamePatchRequest: { + ...options, + workspaceId: options.projectId + } + }, + this.#requestOptions + ); + return res.data; + } catch (err) { + throw newInfisicalError(err); + } }; createSecret = async (secretName: DefaultApiApiV3SecretsRawSecretNamePostRequest["secretName"], options: CreateSecretOptions) => { - const res = await this.#apiInstance.apiV3SecretsRawSecretNamePost( - { - secretName, - apiV3SecretsRawSecretNamePostRequest: { - ...options, - workspaceId: options.projectId - } - }, - this.#requestOptions - ); - return res.data; + try { + const res = await this.#apiInstance.apiV3SecretsRawSecretNamePost( + { + secretName, + apiV3SecretsRawSecretNamePostRequest: { + ...options, + workspaceId: options.projectId + } + }, + this.#requestOptions + ); + return res.data; + } catch (err) { + throw newInfisicalError(err); + } }; deleteSecret = async (secretName: DefaultApiApiV3SecretsRawSecretNameDeleteRequest["secretName"], options: DeleteSecretOptions) => { - const res = await this.#apiInstance.apiV3SecretsRawSecretNameDelete( - { - secretName, - apiV3SecretsRawSecretNameDeleteRequest: { - ...options, - workspaceId: options.projectId - } - }, - this.#requestOptions - ); - return res.data; + try { + const res = await this.#apiInstance.apiV3SecretsRawSecretNameDelete( + { + secretName, + apiV3SecretsRawSecretNameDeleteRequest: { + ...options, + workspaceId: options.projectId + } + }, + this.#requestOptions + ); + return res.data; + } catch (err) { + throw newInfisicalError(err); + } }; } From aefae1c470c8ac4c46587bb14674725866755185 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Thu, 10 Oct 2024 00:57:24 +0400 Subject: [PATCH 2/4] feat: less verbose errors --- src/custom/auth.ts | 86 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 65 insertions(+), 21 deletions(-) diff --git a/src/custom/auth.ts b/src/custom/auth.ts index 81c17c3..b195e58 100644 --- a/src/custom/auth.ts +++ b/src/custom/auth.ts @@ -2,6 +2,7 @@ import { InfisicalSDK } from ".."; import { ApiV1AuthUniversalAuthLoginPostRequest } from "../infisicalapi_client"; import { DefaultApi as InfisicalApi } from "../infisicalapi_client"; import { MACHINE_IDENTITY_ID_ENV_NAME } from "./constants"; +import { InfisicalSDKError, newInfisicalError } from "./errors"; import { getAwsRegion, performAwsIamLogin } from "./util"; type AuthenticatorFunction = (accessToken: string) => InfisicalSDK; @@ -10,47 +11,90 @@ type AwsAuthLoginOptions = { identityId?: string; }; +export const renewToken = async (apiClient: InfisicalApi, token?: string) => { + try { + if (!token) { + throw new InfisicalSDKError("Unable to renew access token, no access token set. Are you sure you're authenticated?"); + } + + const res = await apiClient.apiV1AuthTokenRenewPost({ + apiV1AuthTokenRenewPostRequest: { + accessToken: token + } + }); + + return res.data; + } catch (err) { + throw newInfisicalError(err); + } +}; + export default class AuthClient { #sdkAuthenticator: AuthenticatorFunction; #apiClient: InfisicalApi; - #baseUrl: string; + #accessToken?: string; - constructor(authenticator: AuthenticatorFunction, apiInstance: InfisicalApi, baseUrl: string) { + constructor(authenticator: AuthenticatorFunction, apiInstance: InfisicalApi, accessToken?: string) { this.#sdkAuthenticator = authenticator; this.#apiClient = apiInstance; - this.#baseUrl = baseUrl; + this.#accessToken = accessToken; } awsIamAuth = { login: async (options?: AwsAuthLoginOptions) => { - const identityId = options?.identityId || process.env[MACHINE_IDENTITY_ID_ENV_NAME]; + try { + const identityId = options?.identityId || process.env[MACHINE_IDENTITY_ID_ENV_NAME]; - if (!identityId) { - throw new Error("Identity ID is required for AWS IAM authentication"); - } + if (!identityId) { + throw new InfisicalSDKError("Identity ID is required for AWS IAM authentication"); + } - const iamRequest = await performAwsIamLogin(await getAwsRegion()); + const iamRequest = await performAwsIamLogin(await getAwsRegion()); - const res = await this.#apiClient.apiV1AuthAwsAuthLoginPost({ - apiV1AuthAwsAuthLoginPostRequest: { - iamHttpRequestMethod: iamRequest.iamHttpRequestMethod, - iamRequestBody: Buffer.from(iamRequest.iamRequestBody).toString("base64"), - iamRequestHeaders: Buffer.from(JSON.stringify(iamRequest.iamRequestHeaders)).toString("base64"), - identityId - } - }); + const res = await this.#apiClient.apiV1AuthAwsAuthLoginPost({ + apiV1AuthAwsAuthLoginPostRequest: { + iamHttpRequestMethod: iamRequest.iamHttpRequestMethod, + iamRequestBody: Buffer.from(iamRequest.iamRequestBody).toString("base64"), + iamRequestHeaders: Buffer.from(JSON.stringify(iamRequest.iamRequestHeaders)).toString("base64"), + identityId + } + }); - return this.#sdkAuthenticator(res.data.accessToken); + return this.#sdkAuthenticator(res.data.accessToken); + } catch (err) { + throw newInfisicalError(err); + } + }, + renew: async () => { + try { + const refreshedToken = await renewToken(this.#apiClient, this.#accessToken); + return this.#sdkAuthenticator(refreshedToken.accessToken); + } catch (err) { + throw newInfisicalError(err); + } } }; universalAuth = { login: async (options: ApiV1AuthUniversalAuthLoginPostRequest) => { - const res = await this.#apiClient.apiV1AuthUniversalAuthLoginPost({ - apiV1AuthUniversalAuthLoginPostRequest: options - }); + try { + const res = await this.#apiClient.apiV1AuthUniversalAuthLoginPost({ + apiV1AuthUniversalAuthLoginPostRequest: options + }); - return this.#sdkAuthenticator(res.data.accessToken); + return this.#sdkAuthenticator(res.data.accessToken); + } catch (err) { + throw newInfisicalError(err); + } + }, + + renew: async () => { + try { + const refreshedToken = await renewToken(this.#apiClient, this.#accessToken); + return this.#sdkAuthenticator(refreshedToken.accessToken); + } catch (err) { + throw newInfisicalError(err); + } } }; From c97829aa6774437deaf069a800e9ed6304a0b286 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Thu, 10 Oct 2024 00:57:42 +0400 Subject: [PATCH 3/4] =?UTF-8?q?cleanup=20=F0=9F=A7=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/custom/dynamic-secrets.ts | 97 ++++++++++++++++----------- src/custom/schemas/dynamic-secrets.ts | 17 ++++- src/custom/util.ts | 5 +- src/index.ts | 4 +- 4 files changed, 79 insertions(+), 44 deletions(-) diff --git a/src/custom/dynamic-secrets.ts b/src/custom/dynamic-secrets.ts index 69da766..466f04e 100644 --- a/src/custom/dynamic-secrets.ts +++ b/src/custom/dynamic-secrets.ts @@ -9,6 +9,7 @@ import type { } from "../infisicalapi_client"; import type { TDynamicSecretProvider } from "./schemas/dynamic-secrets"; +import { newInfisicalError } from "./errors"; type CreateDynamicSecretOptions = Omit & { provider: TDynamicSecretProvider; @@ -23,67 +24,87 @@ export default class DynamicSecretsClient { } async create(options: CreateDynamicSecretOptions) { - const res = await this.#apiInstance.apiV1DynamicSecretsPost( - { - apiV1DynamicSecretsPostRequest: options as DefaultApiApiV1DynamicSecretsPostRequest["apiV1DynamicSecretsPostRequest"] - }, - this.#requestOptions - ); + try { + const res = await this.#apiInstance.apiV1DynamicSecretsPost( + { + apiV1DynamicSecretsPostRequest: options as DefaultApiApiV1DynamicSecretsPostRequest["apiV1DynamicSecretsPostRequest"] + }, + this.#requestOptions + ); - return res.data.dynamicSecret; + return res.data.dynamicSecret; + } catch (err) { + throw newInfisicalError(err); + } } async delete(dynamicSecretName: string, options: DefaultApiApiV1DynamicSecretsNameDeleteRequest["apiV1DynamicSecretsNameDeleteRequest"]) { - const res = await this.#apiInstance.apiV1DynamicSecretsNameDelete( - { - name: dynamicSecretName, - apiV1DynamicSecretsNameDeleteRequest: options - }, - this.#requestOptions - ); + try { + const res = await this.#apiInstance.apiV1DynamicSecretsNameDelete( + { + name: dynamicSecretName, + apiV1DynamicSecretsNameDeleteRequest: options + }, + this.#requestOptions + ); - return res.data.dynamicSecret; + return res.data.dynamicSecret; + } catch (err) { + throw newInfisicalError(err); + } } leases = { create: async (options: DefaultApiApiV1DynamicSecretsLeasesPostRequest["apiV1DynamicSecretsLeasesPostRequest"]) => { - const res = await this.#apiInstance.apiV1DynamicSecretsLeasesPost( - { - apiV1DynamicSecretsLeasesPostRequest: options - }, - this.#requestOptions - ); + try { + const res = await this.#apiInstance.apiV1DynamicSecretsLeasesPost( + { + apiV1DynamicSecretsLeasesPostRequest: options + }, + this.#requestOptions + ); - return res.data; + return res.data; + } catch (err) { + throw newInfisicalError(err); + } }, delete: async ( leaseId: string, options: DefaultApiApiV1DynamicSecretsLeasesLeaseIdDeleteRequest["apiV1DynamicSecretsLeasesLeaseIdDeleteRequest"] ) => { - const res = await this.#apiInstance.apiV1DynamicSecretsLeasesLeaseIdDelete( - { - leaseId: leaseId, - apiV1DynamicSecretsLeasesLeaseIdDeleteRequest: options - }, - this.#requestOptions - ); + try { + const res = await this.#apiInstance.apiV1DynamicSecretsLeasesLeaseIdDelete( + { + leaseId: leaseId, + apiV1DynamicSecretsLeasesLeaseIdDeleteRequest: options + }, + this.#requestOptions + ); - return res.data; + return res.data; + } catch (err) { + throw newInfisicalError(err); + } }, renew: async ( leaseId: string, options: DefaultApiApiV1DynamicSecretsLeasesLeaseIdRenewPostRequest["apiV1DynamicSecretsLeasesLeaseIdRenewPostRequest"] ) => { - const res = await this.#apiInstance.apiV1DynamicSecretsLeasesLeaseIdRenewPost( - { - leaseId: leaseId, - apiV1DynamicSecretsLeasesLeaseIdRenewPostRequest: options - }, - this.#requestOptions - ); + try { + const res = await this.#apiInstance.apiV1DynamicSecretsLeasesLeaseIdRenewPost( + { + leaseId: leaseId, + apiV1DynamicSecretsLeasesLeaseIdRenewPostRequest: options + }, + this.#requestOptions + ); - return res.data; + return res.data; + } catch (err) { + throw newInfisicalError(err); + } } }; } diff --git a/src/custom/schemas/dynamic-secrets.ts b/src/custom/schemas/dynamic-secrets.ts index 5822caf..31ac3f6 100644 --- a/src/custom/schemas/dynamic-secrets.ts +++ b/src/custom/schemas/dynamic-secrets.ts @@ -165,6 +165,17 @@ export const AzureEntraIDSchema = z.object({ clientSecret: z.string().trim().min(1) }); +export const LdapSchema = z.object({ + url: z.string().trim().min(1), + binddn: z.string().trim().min(1), + bindpass: z.string().trim().min(1), + ca: z.string().optional(), + + creationLdif: z.string().min(1), + revocationLdif: z.string().min(1), + rollbackLdif: z.string().optional() +}); + export enum DynamicSecretProviders { SqlDatabase = "sql-database", Cassandra = "cassandra", @@ -175,7 +186,8 @@ export enum DynamicSecretProviders { ElasticSearch = "elastic-search", MongoDB = "mongo-db", RabbitMq = "rabbit-mq", - AzureEntraID = "azure-entra-id" + AzureEntraID = "azure-entra-id", + Ldap = "ldap" } export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [ @@ -188,7 +200,8 @@ export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [ z.object({ type: z.literal(DynamicSecretProviders.ElasticSearch), inputs: DynamicSecretElasticSearchSchema }), z.object({ type: z.literal(DynamicSecretProviders.MongoDB), inputs: DynamicSecretMongoDBSchema }), z.object({ type: z.literal(DynamicSecretProviders.RabbitMq), inputs: DynamicSecretRabbitMqSchema }), - z.object({ type: z.literal(DynamicSecretProviders.AzureEntraID), inputs: AzureEntraIDSchema }) + z.object({ type: z.literal(DynamicSecretProviders.AzureEntraID), inputs: AzureEntraIDSchema }), + z.object({ type: z.literal(DynamicSecretProviders.Ldap), inputs: LdapSchema }) ]); export type TDynamicSecretProvider = z.infer; diff --git a/src/custom/util.ts b/src/custom/util.ts index bb6cd67..6a6ccc8 100644 --- a/src/custom/util.ts +++ b/src/custom/util.ts @@ -1,6 +1,7 @@ import axios from "axios"; import { AWS_IDENTITY_DOCUMENT_URI, AWS_TOKEN_METADATA_URI } from "./constants"; import AWS from "aws-sdk"; +import { InfisicalSDKError } from "./errors"; export const getAwsRegion = async () => { const region = process.env.AWS_REGION; // Typically found in lambda runtime environment @@ -36,13 +37,13 @@ export const performAwsIamLogin = async (region: string) => { region }); - const creds = await new Promise<{ sessionToken?: string; accessKeyId: string; secretAccessKey: string }>((resolve, reject) => { + await new Promise<{ sessionToken?: string; accessKeyId: string; secretAccessKey: string }>((resolve, reject) => { AWS.config.getCredentials((err, res) => { if (err) { throw err; } else { if (!res) { - throw new Error("Credentials not found"); + throw new InfisicalSDKError("Credentials not found"); } return resolve(res); } diff --git a/src/index.ts b/src/index.ts index 32d8711..5359b8a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -38,7 +38,7 @@ class InfisicalSDK { }) ); - this.#authClient = new AuthClient(this.authenticate.bind(this), this.#apiInstance, this.#basePath); + this.#authClient = new AuthClient(this.authenticate.bind(this), this.#apiInstance); this.#dynamicSecretsClient = new DynamicSecretsClient(this.#apiInstance, this.#requestOptions); this.#secretsClient = new SecretsClient(this.#apiInstance, this.#requestOptions); this.rest = () => buildRestClient(this.#apiInstance, this.#requestOptions); @@ -61,7 +61,7 @@ class InfisicalSDK { this.rest = () => buildRestClient(this.#apiInstance, this.#requestOptions); this.#secretsClient = new SecretsClient(this.#apiInstance, this.#requestOptions); this.#dynamicSecretsClient = new DynamicSecretsClient(this.#apiInstance, this.#requestOptions); - this.#authClient = new AuthClient(this.authenticate.bind(this), this.#apiInstance, this.#basePath); + this.#authClient = new AuthClient(this.authenticate.bind(this), this.#apiInstance, accessToken); return this; } From d41f4a3bc8ecb905c452cd3e60e8490a789c37cb Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Thu, 10 Oct 2024 00:59:24 +0400 Subject: [PATCH 4/4] =?UTF-8?q?docs=20=E2=9C=8D=EF=B8=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README.md b/README.md index 85b91d7..1231182 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,7 @@ The `Auth` component provides methods for authentication: #### Universal Auth +#### Authenticating ```typescript await client.auth().universalAuth.login({ clientId: "", @@ -56,6 +57,11 @@ await client.auth().universalAuth.login({ - `clientId` (string): The client ID of your Machine Identity. - `clientSecret` (string): The client secret of your Machine Identity. +#### Renewing +You can renew the authentication token that is currently set by using the `renew()` method. +```typescript +await client.auth().universalAuth.renew(); + #### Manually set access token By default, when you run a successful `.login()` method call, the access token returned will be auto set for the client instance. However, if you wish to set the access token manually, you may use this method. @@ -73,6 +79,7 @@ client.auth().accessToken("") > [!NOTE] > AWS IAM auth only works when the SDK is being used from within an AWS service, such as Lambda, EC2, etc. +#### Authenticating ```typescript await client.auth().awsIamAuth.login({ identityId: "" @@ -83,6 +90,13 @@ await client.auth().awsIamAuth.login({ - `options` (object): - `identityId` (string): The ID of your identity +#### Renewing +You can renew the authentication token that is currently set by using the `renew()` method. +```typescript +await client.auth().awsIamAuth.renew(); +``` + + ### `secrets`