Merge pull request #5 from Infisical/daniel/less-verbose-errors

feat: less verbose errors
This commit is contained in:
Daniel Hougaard
2024-10-10 02:09:20 +04:00
committed by GitHub
8 changed files with 298 additions and 131 deletions

View File

@@ -44,6 +44,7 @@ The `Auth` component provides methods for authentication:
#### Universal Auth #### Universal Auth
#### Authenticating
```typescript ```typescript
await client.auth().universalAuth.login({ await client.auth().universalAuth.login({
clientId: "<machine-identity-client-id>", clientId: "<machine-identity-client-id>",
@@ -56,6 +57,11 @@ await client.auth().universalAuth.login({
- `clientId` (string): The client ID of your Machine Identity. - `clientId` (string): The client ID of your Machine Identity.
- `clientSecret` (string): The client secret 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 #### 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. 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("<your-access-token>")
> [!NOTE] > [!NOTE]
> AWS IAM auth only works when the SDK is being used from within an AWS service, such as Lambda, EC2, etc. > AWS IAM auth only works when the SDK is being used from within an AWS service, such as Lambda, EC2, etc.
#### Authenticating
```typescript ```typescript
await client.auth().awsIamAuth.login({ await client.auth().awsIamAuth.login({
identityId: "<your-identity-id>" identityId: "<your-identity-id>"
@@ -83,6 +90,13 @@ await client.auth().awsIamAuth.login({
- `options` (object): - `options` (object):
- `identityId` (string): The ID of your identity - `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` ### `secrets`

View File

@@ -2,6 +2,7 @@ import { InfisicalSDK } from "..";
import { ApiV1AuthUniversalAuthLoginPostRequest } from "../infisicalapi_client"; import { ApiV1AuthUniversalAuthLoginPostRequest } from "../infisicalapi_client";
import { DefaultApi as InfisicalApi } from "../infisicalapi_client"; import { DefaultApi as InfisicalApi } from "../infisicalapi_client";
import { MACHINE_IDENTITY_ID_ENV_NAME } from "./constants"; import { MACHINE_IDENTITY_ID_ENV_NAME } from "./constants";
import { InfisicalSDKError, newInfisicalError } from "./errors";
import { getAwsRegion, performAwsIamLogin } from "./util"; import { getAwsRegion, performAwsIamLogin } from "./util";
type AuthenticatorFunction = (accessToken: string) => InfisicalSDK; type AuthenticatorFunction = (accessToken: string) => InfisicalSDK;
@@ -10,23 +11,42 @@ type AwsAuthLoginOptions = {
identityId?: string; 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 { export default class AuthClient {
#sdkAuthenticator: AuthenticatorFunction; #sdkAuthenticator: AuthenticatorFunction;
#apiClient: InfisicalApi; #apiClient: InfisicalApi;
#baseUrl: string; #accessToken?: string;
constructor(authenticator: AuthenticatorFunction, apiInstance: InfisicalApi, baseUrl: string) { constructor(authenticator: AuthenticatorFunction, apiInstance: InfisicalApi, accessToken?: string) {
this.#sdkAuthenticator = authenticator; this.#sdkAuthenticator = authenticator;
this.#apiClient = apiInstance; this.#apiClient = apiInstance;
this.#baseUrl = baseUrl; this.#accessToken = accessToken;
} }
awsIamAuth = { awsIamAuth = {
login: async (options?: AwsAuthLoginOptions) => { login: async (options?: AwsAuthLoginOptions) => {
try {
const identityId = options?.identityId || process.env[MACHINE_IDENTITY_ID_ENV_NAME]; const identityId = options?.identityId || process.env[MACHINE_IDENTITY_ID_ENV_NAME];
if (!identityId) { if (!identityId) {
throw new Error("Identity ID is required for AWS IAM authentication"); throw new InfisicalSDKError("Identity ID is required for AWS IAM authentication");
} }
const iamRequest = await performAwsIamLogin(await getAwsRegion()); const iamRequest = await performAwsIamLogin(await getAwsRegion());
@@ -41,16 +61,40 @@ export default class AuthClient {
}); });
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 = { universalAuth = {
login: async (options: ApiV1AuthUniversalAuthLoginPostRequest) => { login: async (options: ApiV1AuthUniversalAuthLoginPostRequest) => {
try {
const res = await this.#apiClient.apiV1AuthUniversalAuthLoginPost({ const res = await this.#apiClient.apiV1AuthUniversalAuthLoginPost({
apiV1AuthUniversalAuthLoginPostRequest: options 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);
}
} }
}; };

View File

@@ -9,6 +9,7 @@ import type {
} from "../infisicalapi_client"; } from "../infisicalapi_client";
import type { TDynamicSecretProvider } from "./schemas/dynamic-secrets"; import type { TDynamicSecretProvider } from "./schemas/dynamic-secrets";
import { newInfisicalError } from "./errors";
type CreateDynamicSecretOptions = Omit<DefaultApiApiV1DynamicSecretsPostRequest["apiV1DynamicSecretsPostRequest"], "provider"> & { type CreateDynamicSecretOptions = Omit<DefaultApiApiV1DynamicSecretsPostRequest["apiV1DynamicSecretsPostRequest"], "provider"> & {
provider: TDynamicSecretProvider; provider: TDynamicSecretProvider;
@@ -23,6 +24,7 @@ export default class DynamicSecretsClient {
} }
async create(options: CreateDynamicSecretOptions) { async create(options: CreateDynamicSecretOptions) {
try {
const res = await this.#apiInstance.apiV1DynamicSecretsPost( const res = await this.#apiInstance.apiV1DynamicSecretsPost(
{ {
apiV1DynamicSecretsPostRequest: options as DefaultApiApiV1DynamicSecretsPostRequest["apiV1DynamicSecretsPostRequest"] apiV1DynamicSecretsPostRequest: options as DefaultApiApiV1DynamicSecretsPostRequest["apiV1DynamicSecretsPostRequest"]
@@ -31,9 +33,13 @@ export default class DynamicSecretsClient {
); );
return res.data.dynamicSecret; return res.data.dynamicSecret;
} catch (err) {
throw newInfisicalError(err);
}
} }
async delete(dynamicSecretName: string, options: DefaultApiApiV1DynamicSecretsNameDeleteRequest["apiV1DynamicSecretsNameDeleteRequest"]) { async delete(dynamicSecretName: string, options: DefaultApiApiV1DynamicSecretsNameDeleteRequest["apiV1DynamicSecretsNameDeleteRequest"]) {
try {
const res = await this.#apiInstance.apiV1DynamicSecretsNameDelete( const res = await this.#apiInstance.apiV1DynamicSecretsNameDelete(
{ {
name: dynamicSecretName, name: dynamicSecretName,
@@ -43,10 +49,14 @@ export default class DynamicSecretsClient {
); );
return res.data.dynamicSecret; return res.data.dynamicSecret;
} catch (err) {
throw newInfisicalError(err);
}
} }
leases = { leases = {
create: async (options: DefaultApiApiV1DynamicSecretsLeasesPostRequest["apiV1DynamicSecretsLeasesPostRequest"]) => { create: async (options: DefaultApiApiV1DynamicSecretsLeasesPostRequest["apiV1DynamicSecretsLeasesPostRequest"]) => {
try {
const res = await this.#apiInstance.apiV1DynamicSecretsLeasesPost( const res = await this.#apiInstance.apiV1DynamicSecretsLeasesPost(
{ {
apiV1DynamicSecretsLeasesPostRequest: options apiV1DynamicSecretsLeasesPostRequest: options
@@ -55,11 +65,15 @@ export default class DynamicSecretsClient {
); );
return res.data; return res.data;
} catch (err) {
throw newInfisicalError(err);
}
}, },
delete: async ( delete: async (
leaseId: string, leaseId: string,
options: DefaultApiApiV1DynamicSecretsLeasesLeaseIdDeleteRequest["apiV1DynamicSecretsLeasesLeaseIdDeleteRequest"] options: DefaultApiApiV1DynamicSecretsLeasesLeaseIdDeleteRequest["apiV1DynamicSecretsLeasesLeaseIdDeleteRequest"]
) => { ) => {
try {
const res = await this.#apiInstance.apiV1DynamicSecretsLeasesLeaseIdDelete( const res = await this.#apiInstance.apiV1DynamicSecretsLeasesLeaseIdDelete(
{ {
leaseId: leaseId, leaseId: leaseId,
@@ -69,12 +83,16 @@ export default class DynamicSecretsClient {
); );
return res.data; return res.data;
} catch (err) {
throw newInfisicalError(err);
}
}, },
renew: async ( renew: async (
leaseId: string, leaseId: string,
options: DefaultApiApiV1DynamicSecretsLeasesLeaseIdRenewPostRequest["apiV1DynamicSecretsLeasesLeaseIdRenewPostRequest"] options: DefaultApiApiV1DynamicSecretsLeasesLeaseIdRenewPostRequest["apiV1DynamicSecretsLeasesLeaseIdRenewPostRequest"]
) => { ) => {
try {
const res = await this.#apiInstance.apiV1DynamicSecretsLeasesLeaseIdRenewPost( const res = await this.#apiInstance.apiV1DynamicSecretsLeasesLeaseIdRenewPost(
{ {
leaseId: leaseId, leaseId: leaseId,
@@ -84,6 +102,9 @@ export default class DynamicSecretsClient {
); );
return res.data; return res.data;
} catch (err) {
throw newInfisicalError(err);
}
} }
}; };
} }

52
src/custom/errors.ts Normal file
View File

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

View File

@@ -165,6 +165,17 @@ export const AzureEntraIDSchema = z.object({
clientSecret: z.string().trim().min(1) 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 { export enum DynamicSecretProviders {
SqlDatabase = "sql-database", SqlDatabase = "sql-database",
Cassandra = "cassandra", Cassandra = "cassandra",
@@ -175,7 +186,8 @@ export enum DynamicSecretProviders {
ElasticSearch = "elastic-search", ElasticSearch = "elastic-search",
MongoDB = "mongo-db", MongoDB = "mongo-db",
RabbitMq = "rabbit-mq", RabbitMq = "rabbit-mq",
AzureEntraID = "azure-entra-id" AzureEntraID = "azure-entra-id",
Ldap = "ldap"
} }
export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [ 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.ElasticSearch), inputs: DynamicSecretElasticSearchSchema }),
z.object({ type: z.literal(DynamicSecretProviders.MongoDB), inputs: DynamicSecretMongoDBSchema }), z.object({ type: z.literal(DynamicSecretProviders.MongoDB), inputs: DynamicSecretMongoDBSchema }),
z.object({ type: z.literal(DynamicSecretProviders.RabbitMq), inputs: DynamicSecretRabbitMqSchema }), 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<typeof DynamicSecretProviderSchema>; export type TDynamicSecretProvider = z.infer<typeof DynamicSecretProviderSchema>;

View File

@@ -5,6 +5,7 @@ import type {
DefaultApiApiV3SecretsRawSecretNamePatchRequest, DefaultApiApiV3SecretsRawSecretNamePatchRequest,
DefaultApiApiV3SecretsRawSecretNamePostRequest DefaultApiApiV3SecretsRawSecretNamePostRequest
} from "../infisicalapi_client"; } from "../infisicalapi_client";
import { newInfisicalError } from "./errors";
type SecretType = "shared" | "personal"; type SecretType = "shared" | "personal";
@@ -48,9 +49,11 @@ export default class SecretsClient {
#requestOptions: RawAxiosRequestConfig | undefined; #requestOptions: RawAxiosRequestConfig | undefined;
constructor(apiInstance: InfisicalApi, requestOptions: RawAxiosRequestConfig | undefined) { constructor(apiInstance: InfisicalApi, requestOptions: RawAxiosRequestConfig | undefined) {
this.#apiInstance = apiInstance; this.#apiInstance = apiInstance;
this.#requestOptions = requestOptions;
} }
listSecrets = async (options: ListSecretsOptions) => { listSecrets = async (options: ListSecretsOptions) => {
try {
const res = await this.#apiInstance.apiV3SecretsRawGet( const res = await this.#apiInstance.apiV3SecretsRawGet(
{ {
environment: options.environment, environment: options.environment,
@@ -64,9 +67,13 @@ export default class SecretsClient {
this.#requestOptions this.#requestOptions
); );
return res.data; return res.data;
} catch (err) {
throw newInfisicalError(err);
}
}; };
getSecret = async (options: GetSecretOptions) => { getSecret = async (options: GetSecretOptions) => {
try {
const res = await this.#apiInstance.apiV3SecretsRawSecretNameGet( const res = await this.#apiInstance.apiV3SecretsRawSecretNameGet(
{ {
environment: options.environment, environment: options.environment,
@@ -81,9 +88,13 @@ export default class SecretsClient {
this.#requestOptions this.#requestOptions
); );
return res.data.secret; return res.data.secret;
} catch (err) {
throw newInfisicalError(err);
}
}; };
updateSecret = async (secretName: DefaultApiApiV3SecretsRawSecretNamePatchRequest["secretName"], options: UpdateSecretOptions) => { updateSecret = async (secretName: DefaultApiApiV3SecretsRawSecretNamePatchRequest["secretName"], options: UpdateSecretOptions) => {
try {
const res = await this.#apiInstance.apiV3SecretsRawSecretNamePatch( const res = await this.#apiInstance.apiV3SecretsRawSecretNamePatch(
{ {
secretName, secretName,
@@ -95,9 +106,13 @@ export default class SecretsClient {
this.#requestOptions this.#requestOptions
); );
return res.data; return res.data;
} catch (err) {
throw newInfisicalError(err);
}
}; };
createSecret = async (secretName: DefaultApiApiV3SecretsRawSecretNamePostRequest["secretName"], options: CreateSecretOptions) => { createSecret = async (secretName: DefaultApiApiV3SecretsRawSecretNamePostRequest["secretName"], options: CreateSecretOptions) => {
try {
const res = await this.#apiInstance.apiV3SecretsRawSecretNamePost( const res = await this.#apiInstance.apiV3SecretsRawSecretNamePost(
{ {
secretName, secretName,
@@ -109,9 +124,13 @@ export default class SecretsClient {
this.#requestOptions this.#requestOptions
); );
return res.data; return res.data;
} catch (err) {
throw newInfisicalError(err);
}
}; };
deleteSecret = async (secretName: DefaultApiApiV3SecretsRawSecretNameDeleteRequest["secretName"], options: DeleteSecretOptions) => { deleteSecret = async (secretName: DefaultApiApiV3SecretsRawSecretNameDeleteRequest["secretName"], options: DeleteSecretOptions) => {
try {
const res = await this.#apiInstance.apiV3SecretsRawSecretNameDelete( const res = await this.#apiInstance.apiV3SecretsRawSecretNameDelete(
{ {
secretName, secretName,
@@ -123,5 +142,8 @@ export default class SecretsClient {
this.#requestOptions this.#requestOptions
); );
return res.data; return res.data;
} catch (err) {
throw newInfisicalError(err);
}
}; };
} }

View File

@@ -1,6 +1,7 @@
import axios from "axios"; import axios from "axios";
import { AWS_IDENTITY_DOCUMENT_URI, AWS_TOKEN_METADATA_URI } from "./constants"; import { AWS_IDENTITY_DOCUMENT_URI, AWS_TOKEN_METADATA_URI } from "./constants";
import AWS from "aws-sdk"; import AWS from "aws-sdk";
import { InfisicalSDKError } from "./errors";
export const getAwsRegion = async () => { export const getAwsRegion = async () => {
const region = process.env.AWS_REGION; // Typically found in lambda runtime environment const region = process.env.AWS_REGION; // Typically found in lambda runtime environment
@@ -36,13 +37,13 @@ export const performAwsIamLogin = async (region: string) => {
region 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) => { AWS.config.getCredentials((err, res) => {
if (err) { if (err) {
throw err; throw err;
} else { } else {
if (!res) { if (!res) {
throw new Error("Credentials not found"); throw new InfisicalSDKError("Credentials not found");
} }
return resolve(res); return resolve(res);
} }

View File

@@ -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.#dynamicSecretsClient = new DynamicSecretsClient(this.#apiInstance, this.#requestOptions);
this.#secretsClient = new SecretsClient(this.#apiInstance, this.#requestOptions); this.#secretsClient = new SecretsClient(this.#apiInstance, this.#requestOptions);
this.rest = () => buildRestClient(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.rest = () => buildRestClient(this.#apiInstance, this.#requestOptions);
this.#secretsClient = new SecretsClient(this.#apiInstance, this.#requestOptions); this.#secretsClient = new SecretsClient(this.#apiInstance, this.#requestOptions);
this.#dynamicSecretsClient = new DynamicSecretsClient(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; return this;
} }