Skip to content

Commit

Permalink
Merge pull request #2445 from Infisical/daniel/better-api-errors
Browse files Browse the repository at this point in the history
feat(cli/api): more descriptive api errors & CLI warning when using token auth while being logged in
  • Loading branch information
DanielHougaard authored Sep 19, 2024
2 parents d6b7045 + cee9827 commit 8ace72d
Show file tree
Hide file tree
Showing 9 changed files with 124 additions and 24 deletions.
28 changes: 28 additions & 0 deletions backend/src/server/plugins/error-handler.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ForbiddenError } from "@casl/ability";
import fastifyPlugin from "fastify-plugin";
import { JsonWebTokenError } from "jsonwebtoken";
import { ZodError } from "zod";

import {
Expand All @@ -11,6 +12,12 @@ import {
UnauthorizedError
} from "@app/lib/errors";

enum JWTErrors {
JwtExpired = "jwt expired",
JwtMalformed = "jwt malformed",
InvalidAlgorithm = "invalid algorithm"
}

export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider) => {
server.setErrorHandler((error, req, res) => {
req.log.error(error);
Expand All @@ -36,6 +43,27 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider
status: error.status,
detail: error.detail
});
// Handle JWT errors and make them more human-readable for the end-user.
} else if (error instanceof JsonWebTokenError) {
const message = (() => {
if (error.message === JWTErrors.JwtExpired) {
return "Your token has expired. Please re-authenticate.";
}
if (error.message === JWTErrors.JwtMalformed) {
return "The provided access token is malformed. Please use a valid token or generate a new one and try again.";
}
if (error.message === JWTErrors.InvalidAlgorithm) {
return "The access token is signed with an invalid algorithm. Please provide a valid token and try again.";
}

return error.message;
})();

void res.status(401).send({
statusCode: 401,
error: "TokenError",
message
});
} else {
void res.send(error);
}
Expand Down
5 changes: 4 additions & 1 deletion backend/src/services/project-bot/project-bot-fns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@ export const getBotKeyFnFactory = (
) => {
const getBotKeyFn = async (projectId: string) => {
const project = await projectDAL.findById(projectId);
if (!project) throw new BadRequestError({ message: "Project not found during bot lookup." });
if (!project)
throw new BadRequestError({
message: "Project not found during bot lookup. Are you sure you are using the correct project ID?"
});

if (project.version === 3) {
return { project, shouldUseSecretV2Bridge: true };
Expand Down
6 changes: 5 additions & 1 deletion backend/src/services/secret-import/secret-import-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -512,7 +512,11 @@ export const secretImportServiceFactory = ({
return importedSecrets;
}

if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
if (!botKey)
throw new BadRequestError({
message: "Project bot not found. Please upgrade your project.",
name: "bot_not_found_error"
});

const importedSecrets = await fnSecretsFromImports({ allowedImports, folderDAL, secretDAL, secretImportDAL });
return importedSecrets.map((el) => ({
Expand Down
12 changes: 10 additions & 2 deletions backend/src/services/secret/secret-fns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -832,7 +832,11 @@ export const createManySecretsRawFnFactory = ({
secretDAL
});

if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
if (!botKey)
throw new BadRequestError({
message: "Project bot not found. Please upgrade your project.",
name: "bot_not_found_error"
});
const inputSecrets = secrets.map((secret) => {
const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(secret.secretName, botKey);
const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(secret.secretValue || "", botKey);
Expand Down Expand Up @@ -993,7 +997,11 @@ export const updateManySecretsRawFnFactory = ({
return updatedSecrets;
}

if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
if (!botKey)
throw new BadRequestError({
message: "Project bot not found. Please upgrade your project.",
name: "bot_not_found_error"
});
const blindIndexCfg = await secretBlindIndexDAL.findOne({ projectId });
if (!blindIndexCfg) throw new BadRequestError({ message: "Blind index not found", name: "Update secret" });

Expand Down
57 changes: 47 additions & 10 deletions backend/src/services/secret/secret-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -985,7 +985,11 @@ export const secretServiceFactory = ({
return { secrets, imports };
}

if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
if (!botKey)
throw new BadRequestError({
message: "Project bot not found. Please upgrade your project.",
name: "bot_not_found_error"
});

const { secrets, imports } = await getSecrets({
actorId,
Expand Down Expand Up @@ -1146,7 +1150,10 @@ export const secretServiceFactory = ({
});

if (!botKey)
throw new BadRequestError({ message: "Please upgrade your project first", name: "bot_not_found_error" });
throw new BadRequestError({
message: "Project bot not found. Please upgrade your project.",
name: "bot_not_found_error"
});
const decryptedSecret = decryptSecretRaw(encryptedSecret, botKey);

if (expandSecretReferences) {
Expand Down Expand Up @@ -1238,7 +1245,11 @@ export const secretServiceFactory = ({
return { secret, type: SecretProtectionType.Direct as const };
}

if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
if (!botKey)
throw new BadRequestError({
message: "Project bot not found. Please upgrade your project.",
name: "bot_not_found_error"
});
const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(secretName, botKey);
const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(secretValue || "", botKey);
const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8(secretComment || "", botKey);
Expand Down Expand Up @@ -1376,7 +1387,11 @@ export const secretServiceFactory = ({
return { type: SecretProtectionType.Direct as const, secret };
}

if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
if (!botKey)
throw new BadRequestError({
message: "Project bot not found. Please upgrade your project.",
name: "bot_not_found_error"
});

const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(secretValue || "", botKey);
const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8(secretComment || "", botKey);
Expand Down Expand Up @@ -1498,7 +1513,11 @@ export const secretServiceFactory = ({
});
return { type: SecretProtectionType.Direct as const, secret };
}
if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
if (!botKey)
throw new BadRequestError({
message: "Project bot not found. Please upgrade your project.",
name: "bot_not_found_error"
});
if (policy) {
const approval = await secretApprovalRequestService.generateSecretApprovalRequest({
policy,
Expand Down Expand Up @@ -1598,7 +1617,11 @@ export const secretServiceFactory = ({
return { secrets, type: SecretProtectionType.Direct as const };
}

if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
if (!botKey)
throw new BadRequestError({
message: "Project bot not found. Please upgrade your project.",
name: "bot_not_found_error"
});
const sanitizedSecrets = inputSecrets.map(
({ secretComment, secretKey, metadata, tagIds, secretValue, skipMultilineEncoding }) => {
const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(secretKey, botKey);
Expand Down Expand Up @@ -1720,7 +1743,11 @@ export const secretServiceFactory = ({
return { type: SecretProtectionType.Direct as const, secrets };
}

if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
if (!botKey)
throw new BadRequestError({
message: "Project bot not found. Please upgrade your project.",
name: "bot_not_found_error"
});
const sanitizedSecrets = inputSecrets.map(
({
secretComment,
Expand Down Expand Up @@ -1848,7 +1875,11 @@ export const secretServiceFactory = ({
return { type: SecretProtectionType.Direct as const, secrets };
}

if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
if (!botKey)
throw new BadRequestError({
message: "Project bot not found. Please upgrade your project.",
name: "bot_not_found_error"
});

if (policy) {
const approval = await secretApprovalRequestService.generateSecretApprovalRequest({
Expand Down Expand Up @@ -2182,7 +2213,10 @@ export const secretServiceFactory = ({
}

if (!botKey)
throw new BadRequestError({ message: "Please upgrade your project first", name: "bot_not_found_error" });
throw new BadRequestError({
message: "Project bot not found. Please upgrade your project.",
name: "bot_not_found_error"
});

await secretDAL.transaction(async (tx) => {
const secrets = await secretDAL.findAllProjectSecretValues(projectId, tx);
Expand Down Expand Up @@ -2265,7 +2299,10 @@ export const secretServiceFactory = ({

const { botKey } = await projectBotService.getBotKey(project.id);
if (!botKey) {
throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
throw new BadRequestError({
message: "Project bot not found. Please upgrade your project.",
name: "bot_not_found_error"
});
}

const sourceFolder = await folderDAL.findBySecretPath(project.id, sourceEnvironment, sourceSecretPath);
Expand Down
15 changes: 14 additions & 1 deletion cli/packages/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ Copyright (c) 2023 Infisical Inc.
package cmd

import (
"fmt"
"os"
"strings"

Expand Down Expand Up @@ -43,14 +44,26 @@ func init() {
rootCmd.PersistentFlags().Bool("silent", false, "Disable output of tip/info messages. Useful when running in scripts or CI/CD pipelines.")
rootCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
silent, err := cmd.Flags().GetBool("silent")
config.INFISICAL_URL = util.AppendAPIEndpoint(config.INFISICAL_URL)
if err != nil {
util.HandleError(err)
}

config.INFISICAL_URL = util.AppendAPIEndpoint(config.INFISICAL_URL)

if !util.IsRunningInDocker() && !silent {
util.CheckForUpdate()
}

loggedInDetails, err := util.GetCurrentLoggedInUserDetails()

if !silent && err == nil && loggedInDetails.IsUserLoggedIn && !loggedInDetails.LoginExpired {
token, err := util.GetInfisicalToken(cmd)

if err == nil && token != nil {
util.PrintWarning(fmt.Sprintf("Your logged-in session is being overwritten by the token provided from the %s.", token.Source))
}
}

}

// if config.INFISICAL_URL is set to the default value, check if INFISICAL_URL is set in the environment
Expand Down
6 changes: 3 additions & 3 deletions cli/packages/cmd/secrets.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,19 +160,19 @@ var secretsSetCmd = &cobra.Command{
util.HandleError(err, "Unable to parse flag")
}

if (token == nil) {
if token == nil {
util.RequireLocalWorkspaceFile()
}

environmentName, _ := cmd.Flags().GetString("env")
if !cmd.Flags().Changed("env") {
environmentFromWorkspace := util.GetEnvFromWorkspaceFile()
environmentFromWorkspace := util.GetEnvFromWorkspaceFile()
if environmentFromWorkspace != "" {
environmentName = environmentFromWorkspace
}
}

projectId, err := cmd.Flags().GetString("projectId")
projectId, err := cmd.Flags().GetString("projectId")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
Expand Down
5 changes: 3 additions & 2 deletions cli/packages/models/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,9 @@ type DynamicSecretLease struct {
}

type TokenDetails struct {
Type string
Token string
Type string
Token string
Source string
}

type SingleFolder struct {
Expand Down
14 changes: 10 additions & 4 deletions cli/packages/util/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,15 @@ func GetInfisicalToken(cmd *cobra.Command) (token *models.TokenDetails, err erro
return nil, err
}

var source = "--token flag"

if infisicalToken == "" { // If no flag is passed, we first check for the universal auth access token env variable.
infisicalToken = os.Getenv(INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN_NAME)
source = fmt.Sprintf("%s environment variable", INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN_NAME)

if infisicalToken == "" { // If it's still empty after the first env check, we check for the service token env variable.
infisicalToken = os.Getenv(INFISICAL_TOKEN_NAME)
source = fmt.Sprintf("%s environment variable", INFISICAL_TOKEN_NAME)
}
}

Expand All @@ -101,14 +105,16 @@ func GetInfisicalToken(cmd *cobra.Command) (token *models.TokenDetails, err erro

if strings.HasPrefix(infisicalToken, "st.") {
return &models.TokenDetails{
Type: SERVICE_TOKEN_IDENTIFIER,
Token: infisicalToken,
Type: SERVICE_TOKEN_IDENTIFIER,
Token: infisicalToken,
Source: source,
}, nil
}

return &models.TokenDetails{
Type: UNIVERSAL_AUTH_TOKEN_IDENTIFIER,
Token: infisicalToken,
Type: UNIVERSAL_AUTH_TOKEN_IDENTIFIER,
Token: infisicalToken,
Source: source,
}, nil

}
Expand Down

0 comments on commit 8ace72d

Please sign in to comment.