From 0d295a2824ece3cfa393add30944e2621b921565 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Wed, 20 Nov 2024 00:00:30 +0400 Subject: [PATCH 01/20] fix: application crash on zod api error --- .../components/IdentityClientSecretModal.tsx | 35 +++++++------------ 1 file changed, 12 insertions(+), 23 deletions(-) diff --git a/frontend/src/views/Org/IdentityPage/components/IdentityClientSecretModal.tsx b/frontend/src/views/Org/IdentityPage/components/IdentityClientSecretModal.tsx index 3f4de4fc1..c32f44f91 100644 --- a/frontend/src/views/Org/IdentityPage/components/IdentityClientSecretModal.tsx +++ b/frontend/src/views/Org/IdentityPage/components/IdentityClientSecretModal.tsx @@ -63,32 +63,21 @@ export const IdentityClientSecretModal = ({ popUp, handlePopUpToggle }: Props) = }; const onFormSubmit = async ({ description, ttl, numUsesLimit }: FormData) => { - try { - const { clientSecret } = await createClientSecret({ - identityId: popUpData.identityId, - description, - ttl: Number(ttl), - numUsesLimit: Number(numUsesLimit) - }); + const { clientSecret } = await createClientSecret({ + identityId: popUpData.identityId, + description, + ttl: Number(ttl), + numUsesLimit: Number(numUsesLimit) + }); - setToken(clientSecret); + setToken(clientSecret); - createNotification({ - text: "Successfully created client secret", - type: "success" - }); + createNotification({ + text: "Successfully created client secret", + type: "success" + }); - reset(); - } catch (err) { - console.error(err); - const error = err as any; - const text = error?.response?.data?.message ?? "Failed to create client secret"; - - createNotification({ - text, - type: "error" - }); - } + reset(); }; return ( From 73e0a54518ae3dff5476db950032995d11ff5ed4 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Wed, 20 Nov 2024 00:01:25 +0400 Subject: [PATCH 02/20] feat: request ID support --- backend/src/server/app.ts | 2 + backend/src/server/plugins/error-handler.ts | 79 ++++++++++++------- backend/src/server/routes/sanitizedSchemas.ts | 5 ++ frontend/src/hooks/api/types.ts | 5 +- frontend/src/reactQuery.tsx | 24 +++--- 5 files changed, 75 insertions(+), 40 deletions(-) diff --git a/backend/src/server/app.ts b/backend/src/server/app.ts index cf7dd622a..439514031 100644 --- a/backend/src/server/app.ts +++ b/backend/src/server/app.ts @@ -17,6 +17,7 @@ import { Logger } from "pino"; import { HsmModule } from "@app/ee/services/hsm/hsm-types"; import { TKeyStoreFactory } from "@app/keystore/keystore"; import { getConfig, IS_PACKAGED } from "@app/lib/config/env"; +import { alphaNumericNanoId } from "@app/lib/nanoid"; import { TQueueServiceFactory } from "@app/queue"; import { TSmtpService } from "@app/services/smtp/smtp-service"; @@ -47,6 +48,7 @@ export const main = async ({ db, hsmModule, auditLogDb, smtp, logger, queue, key const server = fastify({ logger: appCfg.NODE_ENV === "test" ? false : logger, + genReqId: () => `req-${alphaNumericNanoId(14)}`, trustProxy: true, connectionTimeout: appCfg.isHsmConfigured ? 90_000 : 30_000, ignoreTrailingSlash: true, diff --git a/backend/src/server/plugins/error-handler.ts b/backend/src/server/plugins/error-handler.ts index c08b2fe4c..e60e245eb 100644 --- a/backend/src/server/plugins/error-handler.ts +++ b/backend/src/server/plugins/error-handler.ts @@ -39,77 +39,96 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider if (error instanceof BadRequestError) { void res .status(HttpStatusCodes.BadRequest) - .send({ statusCode: HttpStatusCodes.BadRequest, message: error.message, error: error.name }); + .send({ requestId: req.id, statusCode: HttpStatusCodes.BadRequest, message: error.message, error: error.name }); } else if (error instanceof NotFoundError) { void res .status(HttpStatusCodes.NotFound) - .send({ statusCode: HttpStatusCodes.NotFound, message: error.message, error: error.name }); + .send({ requestId: req.id, statusCode: HttpStatusCodes.NotFound, message: error.message, error: error.name }); } else if (error instanceof UnauthorizedError) { - void res - .status(HttpStatusCodes.Unauthorized) - .send({ statusCode: HttpStatusCodes.Unauthorized, message: error.message, error: error.name }); + void res.status(HttpStatusCodes.Unauthorized).send({ + requestId: req.id, + statusCode: HttpStatusCodes.Unauthorized, + message: error.message, + error: error.name + }); } else if (error instanceof DatabaseError || error instanceof InternalServerError) { - void res - .status(HttpStatusCodes.InternalServerError) - .send({ statusCode: HttpStatusCodes.InternalServerError, message: "Something went wrong", error: error.name }); + void res.status(HttpStatusCodes.InternalServerError).send({ + requestId: req.id, + statusCode: HttpStatusCodes.InternalServerError, + message: "Something went wrong", + error: error.name + }); } else if (error instanceof GatewayTimeoutError) { - void res - .status(HttpStatusCodes.GatewayTimeout) - .send({ statusCode: HttpStatusCodes.GatewayTimeout, message: error.message, error: error.name }); + void res.status(HttpStatusCodes.GatewayTimeout).send({ + requestId: req.id, + statusCode: HttpStatusCodes.GatewayTimeout, + message: error.message, + error: error.name + }); } else if (error instanceof ZodError) { - void res - .status(HttpStatusCodes.Unauthorized) - .send({ statusCode: HttpStatusCodes.Unauthorized, error: "ValidationFailure", message: error.issues }); + void res.status(HttpStatusCodes.Unauthorized).send({ + requestId: req.id, + statusCode: HttpStatusCodes.Unauthorized, + error: "ValidationFailure", + message: error.issues + }); } else if (error instanceof ForbiddenError) { void res.status(HttpStatusCodes.Forbidden).send({ + requestId: req.id, statusCode: HttpStatusCodes.Forbidden, error: "PermissionDenied", message: `You are not allowed to ${error.action} on ${error.subjectType} - ${JSON.stringify(error.subject)}` }); } else if (error instanceof ForbiddenRequestError) { void res.status(HttpStatusCodes.Forbidden).send({ + requestId: req.id, statusCode: HttpStatusCodes.Forbidden, message: error.message, error: error.name }); } else if (error instanceof RateLimitError) { void res.status(HttpStatusCodes.TooManyRequests).send({ + requestId: req.id, statusCode: HttpStatusCodes.TooManyRequests, message: error.message, error: error.name }); } else if (error instanceof ScimRequestError) { void res.status(error.status).send({ + requestId: req.id, schemas: error.schemas, status: error.status, detail: error.detail }); } else if (error instanceof OidcAuthError) { - void res - .status(HttpStatusCodes.InternalServerError) - .send({ statusCode: HttpStatusCodes.InternalServerError, message: error.message, error: error.name }); + void res.status(HttpStatusCodes.InternalServerError).send({ + requestId: req.id, + statusCode: HttpStatusCodes.InternalServerError, + message: error.message, + error: error.name + }); } else if (error instanceof jwt.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."; - } + let errorMessage = error.message; - return error.message; - })(); + if (error.message === JWTErrors.JwtExpired) { + errorMessage = "Your token has expired. Please re-authenticate."; + } else if (error.message === JWTErrors.JwtMalformed) { + errorMessage = + "The provided access token is malformed. Please use a valid token or generate a new one and try again."; + } else if (error.message === JWTErrors.InvalidAlgorithm) { + errorMessage = + "The access token is signed with an invalid algorithm. Please provide a valid token and try again."; + } void res.status(HttpStatusCodes.Forbidden).send({ + requestId: req.id, statusCode: HttpStatusCodes.Forbidden, error: "TokenError", - message + message: errorMessage }); } else { void res.status(HttpStatusCodes.InternalServerError).send({ + requestId: req.id, statusCode: HttpStatusCodes.InternalServerError, error: "InternalServerError", message: "Something went wrong" diff --git a/backend/src/server/routes/sanitizedSchemas.ts b/backend/src/server/routes/sanitizedSchemas.ts index 87fa2b120..de4a1d848 100644 --- a/backend/src/server/routes/sanitizedSchemas.ts +++ b/backend/src/server/routes/sanitizedSchemas.ts @@ -30,26 +30,31 @@ export const integrationAuthPubSchema = IntegrationAuthsSchema.pick({ export const DefaultResponseErrorsSchema = { 400: z.object({ + requestId: z.string(), statusCode: z.literal(400), message: z.string(), error: z.string() }), 404: z.object({ + requestId: z.string(), statusCode: z.literal(404), message: z.string(), error: z.string() }), 401: z.object({ + requestId: z.string(), statusCode: z.literal(401), message: z.any(), error: z.string() }), 403: z.object({ + requestId: z.string(), statusCode: z.literal(403), message: z.string(), error: z.string() }), 500: z.object({ + requestId: z.string(), statusCode: z.literal(500), message: z.string(), error: z.string() diff --git a/frontend/src/hooks/api/types.ts b/frontend/src/hooks/api/types.ts index 516a5d7cf..9126559d3 100644 --- a/frontend/src/hooks/api/types.ts +++ b/frontend/src/hooks/api/types.ts @@ -50,12 +50,15 @@ export enum ApiErrorTypes { export type TApiErrors = | { + requestId: string; error: ApiErrorTypes.ValidationError; message: ZodIssue[]; statusCode: 403; } - | { error: ApiErrorTypes.ForbiddenError; message: string; statusCode: 401 } + | { requestId: string; error: ApiErrorTypes.ForbiddenError; message: string; statusCode: 403 } + | { requestId: string; error: ApiErrorTypes.UnauthorizedError; message: string; statusCode: 401 } | { + requestId: string; statusCode: 400; message: string; error: ApiErrorTypes.BadRequestError; diff --git a/frontend/src/reactQuery.tsx b/frontend/src/reactQuery.tsx index bf764d2d7..464efe105 100644 --- a/frontend/src/reactQuery.tsx +++ b/frontend/src/reactQuery.tsx @@ -28,20 +28,26 @@ export const queryClient = new QueryClient({ ))} +
Request ID: {serverResponse.requestId}
) }); return; } - if (serverResponse.statusCode === 401) { - createNotification({ - title: "Forbidden Access", - type: "error", - text: serverResponse.message - }); - return; - } - createNotification({ title: "Bad Request", type: "error", text: serverResponse.message }); + + const title = + // eslint-disable-next-line no-nested-ternary + serverResponse.statusCode === 403 + ? "Forbidden Access" + : serverResponse.statusCode === 401 + ? "Unauthorized Access" + : "Bad Request"; + + createNotification({ + title, + type: "error", + text: `${serverResponse.message} [requestId=${serverResponse.requestId}]` + }); } } }), From 7f70f969368d40539faabffae901246f54ea18af Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Wed, 20 Nov 2024 01:06:18 +0400 Subject: [PATCH 03/20] fix: allow preset domains for `infisical login` --- cli/packages/cmd/export.go | 2 +- cli/packages/cmd/init.go | 2 +- cli/packages/cmd/login.go | 57 +++++++++++++++++++++++++++++--- cli/packages/cmd/root.go | 2 +- cli/packages/cmd/secrets.go | 4 +-- cli/packages/cmd/tokens.go | 2 +- cli/packages/util/credentials.go | 18 +++++----- cli/packages/util/folders.go | 6 ++-- cli/packages/util/secrets.go | 2 +- 9 files changed, 73 insertions(+), 22 deletions(-) diff --git a/cli/packages/cmd/export.go b/cli/packages/cmd/export.go index 6f02408fd..b872b0e61 100644 --- a/cli/packages/cmd/export.go +++ b/cli/packages/cmd/export.go @@ -111,7 +111,7 @@ var exportCmd = &cobra.Command{ accessToken = token.Token } else { log.Debug().Msg("GetAllEnvironmentVariables: Trying to fetch secrets using logged in details") - loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails() + loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true) if err != nil { util.HandleError(err) } diff --git a/cli/packages/cmd/init.go b/cli/packages/cmd/init.go index 05655e97c..df6bfcc60 100644 --- a/cli/packages/cmd/init.go +++ b/cli/packages/cmd/init.go @@ -41,7 +41,7 @@ var initCmd = &cobra.Command{ } } - userCreds, err := util.GetCurrentLoggedInUserDetails() + userCreds, err := util.GetCurrentLoggedInUserDetails(true) if err != nil { util.HandleError(err, "Unable to get your login details") } diff --git a/cli/packages/cmd/login.go b/cli/packages/cmd/login.go index fff2ccf31..8f29c907a 100644 --- a/cli/packages/cmd/login.go +++ b/cli/packages/cmd/login.go @@ -154,6 +154,8 @@ var loginCmd = &cobra.Command{ DisableFlagsInUseLine: true, Run: func(cmd *cobra.Command, args []string) { + presetDomain := config.INFISICAL_URL + clearSelfHostedDomains, err := cmd.Flags().GetBool("clear-domains") if err != nil { util.HandleError(err) @@ -198,7 +200,7 @@ var loginCmd = &cobra.Command{ // standalone user auth if loginMethod == "user" { - currentLoggedInUserDetails, err := util.GetCurrentLoggedInUserDetails() + currentLoggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true) // if the key can't be found or there is an error getting current credentials from key ring, allow them to override if err != nil && (strings.Contains(err.Error(), "we couldn't find your logged in details")) { log.Debug().Err(err) @@ -216,11 +218,19 @@ var loginCmd = &cobra.Command{ return } } + + usePresetDomain, err := usePresetDomain(presetDomain) + + if err != nil { + util.HandleError(err) + } + //override domain domainQuery := true if config.INFISICAL_URL_MANUAL_OVERRIDE != "" && config.INFISICAL_URL_MANUAL_OVERRIDE != fmt.Sprintf("%s/api", util.INFISICAL_DEFAULT_EU_URL) && - config.INFISICAL_URL_MANUAL_OVERRIDE != fmt.Sprintf("%s/api", util.INFISICAL_DEFAULT_US_URL) { + config.INFISICAL_URL_MANUAL_OVERRIDE != fmt.Sprintf("%s/api", util.INFISICAL_DEFAULT_US_URL) && + !usePresetDomain { overrideDomain, err := DomainOverridePrompt() if err != nil { util.HandleError(err) @@ -228,7 +238,7 @@ var loginCmd = &cobra.Command{ //if not override set INFISICAL_URL to exported var //set domainQuery to false - if !overrideDomain { + if !overrideDomain && !usePresetDomain { domainQuery = false config.INFISICAL_URL = util.AppendAPIEndpoint(config.INFISICAL_URL_MANUAL_OVERRIDE) config.INFISICAL_LOGIN_URL = fmt.Sprintf("%s/login", strings.TrimSuffix(config.INFISICAL_URL, "/api")) @@ -237,7 +247,7 @@ var loginCmd = &cobra.Command{ } //prompt user to select domain between Infisical cloud and self-hosting - if domainQuery { + if domainQuery && !usePresetDomain { err = askForDomain() if err != nil { util.HandleError(err, "Unable to parse domain url") @@ -526,6 +536,45 @@ func DomainOverridePrompt() (bool, error) { return selectedOption == OVERRIDE, err } +func usePresetDomain(presetDomain string) (bool, error) { + infisicalConfig, err := util.GetConfigFile() + if err != nil { + return false, fmt.Errorf("askForDomain: unable to get config file because [err=%s]", err) + } + + preconfiguredUrl := strings.TrimSuffix(presetDomain, "/api") + + if preconfiguredUrl != "" && preconfiguredUrl != util.INFISICAL_DEFAULT_US_URL && preconfiguredUrl != util.INFISICAL_DEFAULT_EU_URL { + parsedDomain := strings.TrimSuffix(strings.Trim(preconfiguredUrl, "/"), "/api") + + _, err := url.ParseRequestURI(parsedDomain) + if err != nil { + return false, errors.New(fmt.Sprintf("Invalid domain URL: '%s'", parsedDomain)) + } + + config.INFISICAL_URL = fmt.Sprintf("%s/api", parsedDomain) + config.INFISICAL_LOGIN_URL = fmt.Sprintf("%s/login", parsedDomain) + + if !slices.Contains(infisicalConfig.Domains, parsedDomain) { + infisicalConfig.Domains = append(infisicalConfig.Domains, parsedDomain) + err = util.WriteConfigFile(&infisicalConfig) + + if err != nil { + return false, fmt.Errorf("askForDomain: unable to write domains to config file because [err=%s]", err) + } + } + + whilte := color.New(color.FgGreen) + boldWhite := whilte.Add(color.Bold) + time.Sleep(time.Second * 1) + boldWhite.Printf("[INFO] Using domain '%s' from domain flag or INFISICAL_API_URL environment variable\n", parsedDomain) + + return true, nil + } + + return false, nil +} + func askForDomain() error { // query user to choose between Infisical cloud or self-hosting diff --git a/cli/packages/cmd/root.go b/cli/packages/cmd/root.go index c533f3415..04af9cce8 100644 --- a/cli/packages/cmd/root.go +++ b/cli/packages/cmd/root.go @@ -54,7 +54,7 @@ func init() { util.CheckForUpdate() } - loggedInDetails, err := util.GetCurrentLoggedInUserDetails() + loggedInDetails, err := util.GetCurrentLoggedInUserDetails(false) if !silent && err == nil && loggedInDetails.IsUserLoggedIn && !loggedInDetails.LoginExpired { token, err := util.GetInfisicalToken(cmd) diff --git a/cli/packages/cmd/secrets.go b/cli/packages/cmd/secrets.go index eff011c5e..e93d58885 100644 --- a/cli/packages/cmd/secrets.go +++ b/cli/packages/cmd/secrets.go @@ -194,7 +194,7 @@ var secretsSetCmd = &cobra.Command{ projectId = workspaceFile.WorkspaceId } - loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails() + loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true) if err != nil { util.HandleError(err, "unable to authenticate [err=%v]") } @@ -278,7 +278,7 @@ var secretsDeleteCmd = &cobra.Command{ util.RequireLogin() util.RequireLocalWorkspaceFile() - loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails() + loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true) if err != nil { util.HandleError(err, "Unable to authenticate") } diff --git a/cli/packages/cmd/tokens.go b/cli/packages/cmd/tokens.go index e2851f88f..531e622e9 100644 --- a/cli/packages/cmd/tokens.go +++ b/cli/packages/cmd/tokens.go @@ -41,7 +41,7 @@ var tokensCreateCmd = &cobra.Command{ }, Run: func(cmd *cobra.Command, args []string) { // get plain text workspace key - loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails() + loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true) if err != nil { util.HandleError(err, "Unable to retrieve your logged in your details. Please login in then try again") diff --git a/cli/packages/util/credentials.go b/cli/packages/util/credentials.go index cb5b94080..03722dc41 100644 --- a/cli/packages/util/credentials.go +++ b/cli/packages/util/credentials.go @@ -55,7 +55,7 @@ func GetUserCredsFromKeyRing(userEmail string) (credentials models.UserCredentia return userCredentials, err } -func GetCurrentLoggedInUserDetails() (LoggedInUserDetails, error) { +func GetCurrentLoggedInUserDetails(setConfigVariables bool) (LoggedInUserDetails, error) { if ConfigFileExists() { configFile, err := GetConfigFile() if err != nil { @@ -75,18 +75,20 @@ func GetCurrentLoggedInUserDetails() (LoggedInUserDetails, error) { } } + if setConfigVariables { + config.INFISICAL_URL_MANUAL_OVERRIDE = config.INFISICAL_URL + //configFile.LoggedInUserDomain + //if not empty set as infisical url + if configFile.LoggedInUserDomain != "" { + config.INFISICAL_URL = AppendAPIEndpoint(configFile.LoggedInUserDomain) + } + } + // check to to see if the JWT is still valid httpClient := resty.New(). SetAuthToken(userCreds.JTWToken). SetHeader("Accept", "application/json") - config.INFISICAL_URL_MANUAL_OVERRIDE = config.INFISICAL_URL - //configFile.LoggedInUserDomain - //if not empty set as infisical url - if configFile.LoggedInUserDomain != "" { - config.INFISICAL_URL = AppendAPIEndpoint(configFile.LoggedInUserDomain) - } - isAuthenticated := api.CallIsAuthenticated(httpClient) // TODO: add refresh token // if !isAuthenticated { diff --git a/cli/packages/util/folders.go b/cli/packages/util/folders.go index c7f6de630..4715c71c3 100644 --- a/cli/packages/util/folders.go +++ b/cli/packages/util/folders.go @@ -20,7 +20,7 @@ func GetAllFolders(params models.GetAllFoldersParameters) ([]models.SingleFolder log.Debug().Msg("GetAllFolders: Trying to fetch folders using logged in details") - loggedInUserDetails, err := GetCurrentLoggedInUserDetails() + loggedInUserDetails, err := GetCurrentLoggedInUserDetails(true) if err != nil { return nil, err } @@ -177,7 +177,7 @@ func CreateFolder(params models.CreateFolderParameters) (models.SingleFolder, er if params.InfisicalToken == "" { RequireLogin() RequireLocalWorkspaceFile() - loggedInUserDetails, err := GetCurrentLoggedInUserDetails() + loggedInUserDetails, err := GetCurrentLoggedInUserDetails(true) if err != nil { return models.SingleFolder{}, err @@ -224,7 +224,7 @@ func DeleteFolder(params models.DeleteFolderParameters) ([]models.SingleFolder, RequireLogin() RequireLocalWorkspaceFile() - loggedInUserDetails, err := GetCurrentLoggedInUserDetails() + loggedInUserDetails, err := GetCurrentLoggedInUserDetails(true) if err != nil { return nil, err diff --git a/cli/packages/util/secrets.go b/cli/packages/util/secrets.go index 5e19ea664..5a2a0ec24 100644 --- a/cli/packages/util/secrets.go +++ b/cli/packages/util/secrets.go @@ -246,7 +246,7 @@ func GetAllEnvironmentVariables(params models.GetAllSecretsParameters, projectCo log.Debug().Msg("GetAllEnvironmentVariables: Trying to fetch secrets using logged in details") - loggedInUserDetails, err := GetCurrentLoggedInUserDetails() + loggedInUserDetails, err := GetCurrentLoggedInUserDetails(true) isConnected := ValidateInfisicalAPIConnection() if isConnected { From 38917327d9577fb22aeeb484a1235a6d3a1e1a6c Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Fri, 22 Nov 2024 23:19:07 +0400 Subject: [PATCH 04/20] feat: request lifecycle request ID --- backend/package-lock.json | 10 +++ backend/package.json | 1 + .../src/@types/fastify-request-context.d.ts | 7 ++ backend/src/@types/fastify-zod.d.ts | 4 +- backend/src/ee/services/hsm/hsm-fns.ts | 4 +- .../ee/services/license/license-service.ts | 4 +- .../services/rate-limit/rate-limit-service.ts | 6 +- .../secret-scanning-queue.ts | 4 +- backend/src/lib/config/env.ts | 4 +- backend/src/lib/logger/logger.ts | 67 ++++++++++++++++++- backend/src/server/app.ts | 13 +++- backend/src/server/plugins/secret-scanner.ts | 2 +- backend/src/services/project/project-queue.ts | 17 +++-- backend/src/services/webhook/webhook-fns.ts | 4 +- 14 files changed, 119 insertions(+), 28 deletions(-) create mode 100644 backend/src/@types/fastify-request-context.d.ts diff --git a/backend/package-lock.json b/backend/package-lock.json index 0d7544314..ef7203d2c 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -24,6 +24,7 @@ "@fastify/multipart": "8.3.0", "@fastify/passport": "^2.4.0", "@fastify/rate-limit": "^9.0.0", + "@fastify/request-context": "^5.1.0", "@fastify/session": "^10.7.0", "@fastify/swagger": "^8.14.0", "@fastify/swagger-ui": "^2.1.0", @@ -5528,6 +5529,15 @@ "toad-cache": "^3.3.0" } }, + "node_modules/@fastify/request-context": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@fastify/request-context/-/request-context-5.1.0.tgz", + "integrity": "sha512-PM7wrLJOEylVDpxabOFLaYsdAiaa0lpDUcP2HMFJ1JzgiWuC6k4r3duf6Pm9YLnzlGmT+Yp4tkQjqsu7V/pSOA==", + "license": "MIT", + "dependencies": { + "fastify-plugin": "^4.0.0" + } + }, "node_modules/@fastify/send": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@fastify/send/-/send-2.1.0.tgz", diff --git a/backend/package.json b/backend/package.json index 82b2df024..7db48d56d 100644 --- a/backend/package.json +++ b/backend/package.json @@ -132,6 +132,7 @@ "@fastify/multipart": "8.3.0", "@fastify/passport": "^2.4.0", "@fastify/rate-limit": "^9.0.0", + "@fastify/request-context": "^5.1.0", "@fastify/session": "^10.7.0", "@fastify/swagger": "^8.14.0", "@fastify/swagger-ui": "^2.1.0", diff --git a/backend/src/@types/fastify-request-context.d.ts b/backend/src/@types/fastify-request-context.d.ts new file mode 100644 index 000000000..caef4d5b2 --- /dev/null +++ b/backend/src/@types/fastify-request-context.d.ts @@ -0,0 +1,7 @@ +import "@fastify/request-context"; + +declare module "@fastify/request-context" { + interface RequestContextData { + requestId: string; + } +} diff --git a/backend/src/@types/fastify-zod.d.ts b/backend/src/@types/fastify-zod.d.ts index 393579391..440e3393f 100644 --- a/backend/src/@types/fastify-zod.d.ts +++ b/backend/src/@types/fastify-zod.d.ts @@ -1,6 +1,6 @@ import { FastifyInstance, RawReplyDefaultExpression, RawRequestDefaultExpression, RawServerDefault } from "fastify"; -import { Logger } from "pino"; +import { CustomLogger } from "@app/lib/logger/logger"; import { ZodTypeProvider } from "@app/server/plugins/fastify-zod"; declare global { @@ -8,7 +8,7 @@ declare global { RawServerDefault, RawRequestDefaultExpression, RawReplyDefaultExpression, - Readonly, + Readonly, ZodTypeProvider >; diff --git a/backend/src/ee/services/hsm/hsm-fns.ts b/backend/src/ee/services/hsm/hsm-fns.ts index f91f9a004..3124e1012 100644 --- a/backend/src/ee/services/hsm/hsm-fns.ts +++ b/backend/src/ee/services/hsm/hsm-fns.ts @@ -27,7 +27,7 @@ export const initializeHsmModule = () => { logger.info("PKCS#11 module initialized"); } catch (err) { - logger.error("Failed to initialize PKCS#11 module:", err); + logger.error(err, "Failed to initialize PKCS#11 module"); throw err; } }; @@ -39,7 +39,7 @@ export const initializeHsmModule = () => { isInitialized = false; logger.info("PKCS#11 module finalized"); } catch (err) { - logger.error("Failed to finalize PKCS#11 module:", err); + logger.error(err, "Failed to finalize PKCS#11 module"); throw err; } } diff --git a/backend/src/ee/services/license/license-service.ts b/backend/src/ee/services/license/license-service.ts index dc56e7bc3..6becaaf2b 100644 --- a/backend/src/ee/services/license/license-service.ts +++ b/backend/src/ee/services/license/license-service.ts @@ -161,8 +161,8 @@ export const licenseServiceFactory = ({ } } catch (error) { logger.error( - `getPlan: encountered an error when fetching pan [orgId=${orgId}] [projectId=${projectId}] [error]`, - error + error, + `getPlan: encountered an error when fetching pan [orgId=${orgId}] [projectId=${projectId}] [error]` ); await keyStore.setItemWithExpiry( FEATURE_CACHE_KEY(orgId), diff --git a/backend/src/ee/services/rate-limit/rate-limit-service.ts b/backend/src/ee/services/rate-limit/rate-limit-service.ts index 208fa8428..61b18be91 100644 --- a/backend/src/ee/services/rate-limit/rate-limit-service.ts +++ b/backend/src/ee/services/rate-limit/rate-limit-service.ts @@ -46,7 +46,7 @@ export const rateLimitServiceFactory = ({ rateLimitDAL, licenseService }: TRateL } return rateLimit; } catch (err) { - logger.error("Error fetching rate limits %o", err); + logger.error(err, "Error fetching rate limits"); return undefined; } }; @@ -69,12 +69,12 @@ export const rateLimitServiceFactory = ({ rateLimitDAL, licenseService }: TRateL mfaRateLimit: rateLimit.mfaRateLimit }; - logger.info(`syncRateLimitConfiguration: rate limit configuration: %o`, newRateLimitMaxConfiguration); + logger.info(newRateLimitMaxConfiguration, "syncRateLimitConfiguration: rate limit configuration"); Object.freeze(newRateLimitMaxConfiguration); rateLimitMaxConfiguration = newRateLimitMaxConfiguration; } } catch (error) { - logger.error(`Error syncing rate limit configurations: %o`, error); + logger.error(error, "Error syncing rate limit configurations"); } }; diff --git a/backend/src/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-queue.ts b/backend/src/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-queue.ts index 1907ddd9a..42ff90055 100644 --- a/backend/src/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-queue.ts +++ b/backend/src/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-queue.ts @@ -238,11 +238,11 @@ export const secretScanningQueueFactory = ({ }); queueService.listen(QueueName.SecretPushEventScan, "failed", (job, err) => { - logger.error("Failed to secret scan on push", job?.data, err); + logger.error(err, "Failed to secret scan on push", job?.data); }); queueService.listen(QueueName.SecretFullRepoScan, "failed", (job, err) => { - logger.error("Failed to do full repo secret scan", job?.data, err); + logger.error(err, "Failed to do full repo secret scan", job?.data); }); return { startFullRepoScan, startPushEventScan }; diff --git a/backend/src/lib/config/env.ts b/backend/src/lib/config/env.ts index 279ca057d..12ab33118 100644 --- a/backend/src/lib/config/env.ts +++ b/backend/src/lib/config/env.ts @@ -1,7 +1,7 @@ -import { Logger } from "pino"; import { z } from "zod"; import { removeTrailingSlash } from "../fn"; +import { CustomLogger } from "../logger/logger"; import { zpStr } from "../zod"; export const GITLAB_URL = "https://gitlab.com"; @@ -212,7 +212,7 @@ let envCfg: Readonly>; export const getConfig = () => envCfg; // cannot import singleton logger directly as it needs config to load various transport -export const initEnvConfig = (logger?: Logger) => { +export const initEnvConfig = (logger?: CustomLogger) => { const parsedEnv = envSchema.safeParse(process.env); if (!parsedEnv.success) { (logger ?? console).error("Invalid environment variables. Check the error below"); diff --git a/backend/src/lib/logger/logger.ts b/backend/src/lib/logger/logger.ts index 942efc40a..5563499e5 100644 --- a/backend/src/lib/logger/logger.ts +++ b/backend/src/lib/logger/logger.ts @@ -1,6 +1,8 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ // logger follows a singleton pattern // easier to use it that's all. +import { requestContext } from "@fastify/request-context"; import pino, { Logger } from "pino"; import { z } from "zod"; @@ -13,14 +15,37 @@ const logLevelToSeverityLookup: Record = { "60": "CRITICAL" }; -// eslint-disable-next-line import/no-mutable-exports -export let logger: Readonly; // akhilmhdh: // The logger is not placed in the main app config to avoid a circular dependency. // The config requires the logger to display errors when an invalid environment is supplied. // On the other hand, the logger needs the config to obtain credentials for AWS or other transports. // By keeping the logger separate, it becomes an independent package. +// We define our own custom logger interface to enforce structure to the logging methods. + +export interface CustomLogger extends Omit { + info: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (obj: unknown, msg?: string, ...args: any[]): void; + }; + + error: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (obj: unknown, msg?: string, ...args: any[]): void; + }; + warn: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (obj: unknown, msg?: string, ...args: any[]): void; + }; + debug: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (obj: unknown, msg?: string, ...args: any[]): void; + }; +} + +// eslint-disable-next-line import/no-mutable-exports +export let logger: Readonly; + const loggerConfig = z.object({ AWS_CLOUDWATCH_LOG_GROUP_NAME: z.string().default("infisical-log-stream"), AWS_CLOUDWATCH_LOG_REGION: z.string().default("us-east-1"), @@ -62,6 +87,17 @@ const redactedKeys = [ "config" ]; +const UNKNOWN_REQUEST_ID = "UNKNOWN_REQUEST_ID"; + +const extractRequestId = () => { + try { + return requestContext.get("requestId") || UNKNOWN_REQUEST_ID; + } catch (err) { + console.log("failed to get request context", err); + return UNKNOWN_REQUEST_ID; + } +}; + export const initLogger = async () => { const cfg = loggerConfig.parse(process.env); const targets: pino.TransportMultiOptions["targets"][number][] = [ @@ -94,6 +130,30 @@ export const initLogger = async () => { targets }); + const wrapLogger = (originalLogger: Logger): CustomLogger => { + // eslint-disable-next-line no-param-reassign, @typescript-eslint/no-explicit-any + originalLogger.info = (obj: unknown, msg?: string, ...args: any[]) => { + return originalLogger.child({ requestId: extractRequestId() }).info(obj, msg, ...args); + }; + + // eslint-disable-next-line no-param-reassign, @typescript-eslint/no-explicit-any + originalLogger.error = (obj: unknown, msg?: string, ...args: any[]) => { + return originalLogger.child({ requestId: extractRequestId() }).error(obj, msg, ...args); + }; + + // eslint-disable-next-line no-param-reassign, @typescript-eslint/no-explicit-any + originalLogger.warn = (obj: unknown, msg?: string, ...args: any[]) => { + return originalLogger.child({ requestId: extractRequestId() }).warn(obj, msg, ...args); + }; + + // eslint-disable-next-line no-param-reassign, @typescript-eslint/no-explicit-any + originalLogger.debug = (obj: unknown, msg?: string, ...args: any[]) => { + return originalLogger.child({ requestId: extractRequestId() }).debug(obj, msg, ...args); + }; + + return originalLogger; + }; + logger = pino( { mixin(_context, level) { @@ -113,5 +173,6 @@ export const initLogger = async () => { // eslint-disable-next-line @typescript-eslint/no-unsafe-argument transport ); - return logger; + + return wrapLogger(logger); }; diff --git a/backend/src/server/app.ts b/backend/src/server/app.ts index 439514031..83c34e5a7 100644 --- a/backend/src/server/app.ts +++ b/backend/src/server/app.ts @@ -10,13 +10,14 @@ import fastifyFormBody from "@fastify/formbody"; import helmet from "@fastify/helmet"; import type { FastifyRateLimitOptions } from "@fastify/rate-limit"; import ratelimiter from "@fastify/rate-limit"; +import { fastifyRequestContext } from "@fastify/request-context"; import fastify from "fastify"; import { Knex } from "knex"; -import { Logger } from "pino"; import { HsmModule } from "@app/ee/services/hsm/hsm-types"; import { TKeyStoreFactory } from "@app/keystore/keystore"; import { getConfig, IS_PACKAGED } from "@app/lib/config/env"; +import { CustomLogger } from "@app/lib/logger/logger"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { TQueueServiceFactory } from "@app/queue"; import { TSmtpService } from "@app/services/smtp/smtp-service"; @@ -36,7 +37,7 @@ type TMain = { auditLogDb?: Knex; db: Knex; smtp: TSmtpService; - logger?: Logger; + logger?: CustomLogger; queue: TQueueServiceFactory; keyStore: TKeyStoreFactory; hsmModule: HsmModule; @@ -50,6 +51,7 @@ export const main = async ({ db, hsmModule, auditLogDb, smtp, logger, queue, key logger: appCfg.NODE_ENV === "test" ? false : logger, genReqId: () => `req-${alphaNumericNanoId(14)}`, trustProxy: true, + connectionTimeout: appCfg.isHsmConfigured ? 90_000 : 30_000, ignoreTrailingSlash: true, pluginTimeout: 40_000 @@ -106,6 +108,13 @@ export const main = async ({ db, hsmModule, auditLogDb, smtp, logger, queue, key await server.register(maintenanceMode); + await server.register(fastifyRequestContext, { + defaultStoreValues: (request) => ({ + requestId: request.id, + log: request.log.child({ requestId: request.id }) + }) + }); + await server.register(registerRoutes, { smtp, queue, db, auditLogDb, keyStore, hsmModule }); if (appCfg.isProductionMode) { diff --git a/backend/src/server/plugins/secret-scanner.ts b/backend/src/server/plugins/secret-scanner.ts index d20008de7..d9b5801b9 100644 --- a/backend/src/server/plugins/secret-scanner.ts +++ b/backend/src/server/plugins/secret-scanner.ts @@ -19,7 +19,7 @@ export const registerSecretScannerGhApp = async (server: FastifyZodProvider) => app.on("installation", async (context) => { const { payload } = context; - logger.info("Installed secret scanner to:", { repositories: payload.repositories }); + logger.info({ repositories: payload.repositories }, "Installed secret scanner to"); }); app.on("push", async (context) => { diff --git a/backend/src/services/project/project-queue.ts b/backend/src/services/project/project-queue.ts index d59bde6c1..e845ebd35 100644 --- a/backend/src/services/project/project-queue.ts +++ b/backend/src/services/project/project-queue.ts @@ -285,11 +285,14 @@ export const projectQueueFactory = ({ if (!orgMembership) { // This can happen. Since we don't remove project memberships and project keys when a user is removed from an org, this is a valid case. - logger.info("User is not in organization", { - userId: key.receiverId, - orgId: project.orgId, - projectId: project.id - }); + logger.info( + { + userId: key.receiverId, + orgId: project.orgId, + projectId: project.id + }, + "User is not in organization" + ); // eslint-disable-next-line no-continue continue; } @@ -551,10 +554,10 @@ export const projectQueueFactory = ({ .catch(() => [null]); if (!project) { - logger.error("Failed to upgrade project, because no project was found", data); + logger.error(data, "Failed to upgrade project, because no project was found"); } else { await projectDAL.setProjectUpgradeStatus(data.projectId, ProjectUpgradeStatus.Failed); - logger.error("Failed to upgrade project", err, { + logger.error(err, "Failed to upgrade project", { extra: { project, jobData: data diff --git a/backend/src/services/webhook/webhook-fns.ts b/backend/src/services/webhook/webhook-fns.ts index ffa4b4a04..58f51f880 100644 --- a/backend/src/services/webhook/webhook-fns.ts +++ b/backend/src/services/webhook/webhook-fns.ts @@ -142,7 +142,7 @@ export const fnTriggerWebhook = async ({ !isDisabled && picomatch.isMatch(secretPath, hookSecretPath, { strictSlashes: false }) ); if (!toBeTriggeredHooks.length) return; - logger.info("Secret webhook job started", { environment, secretPath, projectId }); + logger.info({ environment, secretPath, projectId }, "Secret webhook job started"); const project = await projectDAL.findById(projectId); const webhooksTriggered = await Promise.allSettled( toBeTriggeredHooks.map((hook) => @@ -195,5 +195,5 @@ export const fnTriggerWebhook = async ({ ); } }); - logger.info("Secret webhook job ended", { environment, secretPath, projectId }); + logger.info({ environment, secretPath, projectId }, "Secret webhook job ended"); }; From 24b50651c9416d726612ccfc28e092a233ef8603 Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Fri, 22 Nov 2024 13:02:00 -0800 Subject: [PATCH 05/20] fix: correct update role mapping for identity/user and prevent updating role slug to "custom" --- .../identity-project-service.ts | 7 +++- .../IdentityRoleDetailsSection.tsx | 27 ++++++++++++++-- .../MemberRoleDetailsSection.tsx | 32 +++++++++++++++++-- 3 files changed, 60 insertions(+), 6 deletions(-) diff --git a/backend/src/services/identity-project/identity-project-service.ts b/backend/src/services/identity-project/identity-project-service.ts index a49b15c1b..10274f001 100644 --- a/backend/src/services/identity-project/identity-project-service.ts +++ b/backend/src/services/identity-project/identity-project-service.ts @@ -182,7 +182,12 @@ export const identityProjectServiceFactory = ({ // validate custom roles input const customInputRoles = roles.filter( - ({ role }) => !Object.values(ProjectMembershipRole).includes(role as ProjectMembershipRole) + ({ role }) => + !Object.values(ProjectMembershipRole) + // we don't want to include custom in this check; + // this unintentionally lets users set slug to custom which is reserved + .filter((r) => r !== ProjectMembershipRole.Custom) + .includes(role as ProjectMembershipRole) ); const hasCustomRole = Boolean(customInputRoles.length); const customRoles = hasCustomRole diff --git a/frontend/src/views/Project/IdentityDetailsPage/components/IdentityRoleDetailsSection/IdentityRoleDetailsSection.tsx b/frontend/src/views/Project/IdentityDetailsPage/components/IdentityRoleDetailsSection/IdentityRoleDetailsSection.tsx index 8e157fcab..300114228 100644 --- a/frontend/src/views/Project/IdentityDetailsPage/components/IdentityRoleDetailsSection/IdentityRoleDetailsSection.tsx +++ b/frontend/src/views/Project/IdentityDetailsPage/components/IdentityRoleDetailsSection/IdentityRoleDetailsSection.tsx @@ -50,11 +50,34 @@ export const IdentityRoleDetailsSection = ({ const handleRoleDelete = async () => { const { id } = popUp?.deleteRole?.data as TProjectRole; try { - const updatedRole = identityMembershipDetails?.roles?.filter((el) => el.id !== id); + const updatedRoles = identityMembershipDetails?.roles?.filter((el) => el.id !== id); await updateIdentityWorkspaceRole({ workspaceId: currentWorkspace?.id || "", identityId: identityMembershipDetails.identity.id, - roles: updatedRole + roles: updatedRoles.map( + ({ + role, + customRoleSlug, + isTemporary, + temporaryMode, + temporaryRange, + temporaryAccessStartTime, + temporaryAccessEndTime + }) => ({ + role: role === "custom" ? customRoleSlug : role, + ...(isTemporary + ? { + isTemporary, + temporaryMode, + temporaryRange, + temporaryAccessStartTime, + temporaryAccessEndTime + } + : { + isTemporary + }) + }) + ) }); createNotification({ type: "success", text: "Successfully removed role" }); handlePopUpClose("deleteRole"); diff --git a/frontend/src/views/Project/MemberDetailsPage/components/MemberRoleDetailsSection/MemberRoleDetailsSection.tsx b/frontend/src/views/Project/MemberDetailsPage/components/MemberRoleDetailsSection/MemberRoleDetailsSection.tsx index e854ebc59..5ff78360d 100644 --- a/frontend/src/views/Project/MemberDetailsPage/components/MemberRoleDetailsSection/MemberRoleDetailsSection.tsx +++ b/frontend/src/views/Project/MemberDetailsPage/components/MemberRoleDetailsSection/MemberRoleDetailsSection.tsx @@ -61,10 +61,33 @@ export const MemberRoleDetailsSection = ({ const handleRoleDelete = async () => { const { id } = popUp?.deleteRole?.data as TProjectRole; try { - const updatedRole = membershipDetails?.roles?.filter((el) => el.id !== id); + const updatedRoles = membershipDetails?.roles?.filter((el) => el.id !== id); await updateUserWorkspaceRole({ workspaceId: currentWorkspace?.id || "", - roles: updatedRole, + roles: updatedRoles.map( + ({ + role, + customRoleSlug, + isTemporary, + temporaryMode, + temporaryRange, + temporaryAccessStartTime, + temporaryAccessEndTime + }) => ({ + role: role === "custom" ? customRoleSlug : role, + ...(isTemporary + ? { + isTemporary, + temporaryMode, + temporaryRange, + temporaryAccessStartTime, + temporaryAccessEndTime + } + : { + isTemporary + }) + }) + ), membershipId: membershipDetails.id }); createNotification({ type: "success", text: "Successfully removed role" }); @@ -215,7 +238,10 @@ export const MemberRoleDetailsSection = ({ title="Roles" subTitle="Select one or more of the pre-defined or custom roles to configure project permissions." > - + From e3eb14bfd9779a95aee7bb4da64555d81cb30aff Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Fri, 22 Nov 2024 13:09:47 -0800 Subject: [PATCH 06/20] fix: add custom slug check to user --- .../services/identity-project/identity-project-service.ts | 2 +- .../project-membership/project-membership-service.ts | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/backend/src/services/identity-project/identity-project-service.ts b/backend/src/services/identity-project/identity-project-service.ts index 10274f001..7f9cf920e 100644 --- a/backend/src/services/identity-project/identity-project-service.ts +++ b/backend/src/services/identity-project/identity-project-service.ts @@ -185,7 +185,7 @@ export const identityProjectServiceFactory = ({ ({ role }) => !Object.values(ProjectMembershipRole) // we don't want to include custom in this check; - // this unintentionally lets users set slug to custom which is reserved + // this unintentionally enables setting slug to custom which is reserved .filter((r) => r !== ProjectMembershipRole.Custom) .includes(role as ProjectMembershipRole) ); diff --git a/backend/src/services/project-membership/project-membership-service.ts b/backend/src/services/project-membership/project-membership-service.ts index 74b830c6d..b4826b54b 100644 --- a/backend/src/services/project-membership/project-membership-service.ts +++ b/backend/src/services/project-membership/project-membership-service.ts @@ -280,7 +280,12 @@ export const projectMembershipServiceFactory = ({ // validate custom roles input const customInputRoles = roles.filter( - ({ role }) => !Object.values(ProjectMembershipRole).includes(role as ProjectMembershipRole) + ({ role }) => + !Object.values(ProjectMembershipRole) + // we don't want to include custom in this check; + // this unintentionally enables setting slug to custom which is reserved + .filter((r) => r !== ProjectMembershipRole.Custom) + .includes(role as ProjectMembershipRole) ); const hasCustomRole = Boolean(customInputRoles.length); if (hasCustomRole) { From a6921485976a17c97080050eacb636cb7d35b491 Mon Sep 17 00:00:00 2001 From: McPizza Date: Sat, 23 Nov 2024 00:04:33 +0100 Subject: [PATCH 07/20] feat(integrations): Add AWS Secrets Manager IAM Role + Region (#2778) --- .../server/routes/v1/integration-router.ts | 28 +++++++++++++ .../integration/integration-service.ts | 42 +++++++++++++++++++ frontend/src/hooks/api/integrations/types.ts | 3 ++ .../components/IntegrationSettingsSection.tsx | 4 +- 4 files changed, 76 insertions(+), 1 deletion(-) diff --git a/backend/src/server/routes/v1/integration-router.ts b/backend/src/server/routes/v1/integration-router.ts index 86d321852..40141e2c0 100644 --- a/backend/src/server/routes/v1/integration-router.ts +++ b/backend/src/server/routes/v1/integration-router.ts @@ -9,6 +9,7 @@ import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; import { IntegrationMetadataSchema } from "@app/services/integration/integration-schema"; +import { Integrations } from "@app/services/integration-auth/integration-list"; import { PostHogEventTypes, TIntegrationCreatedEvent } from "@app/services/telemetry/telemetry-types"; import {} from "../sanitizedSchemas"; @@ -206,6 +207,33 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => { id: req.params.integrationId }); + if (integration.region) { + integration.metadata = { + ...(integration.metadata || {}), + region: integration.region + }; + } + + if ( + integration.integration === Integrations.AWS_SECRET_MANAGER || + integration.integration === Integrations.AWS_PARAMETER_STORE + ) { + const awsRoleDetails = await server.services.integration.getIntegrationAWSIamRole({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + id: req.params.integrationId + }); + + if (awsRoleDetails) { + integration.metadata = { + ...(integration.metadata || {}), + awsIamRole: awsRoleDetails.role + }; + } + } + return { integration }; } }); diff --git a/backend/src/services/integration/integration-service.ts b/backend/src/services/integration/integration-service.ts index 12f4c77de..1db10405d 100644 --- a/backend/src/services/integration/integration-service.ts +++ b/backend/src/services/integration/integration-service.ts @@ -9,6 +9,7 @@ import { TIntegrationAuthDALFactory } from "../integration-auth/integration-auth import { TIntegrationAuthServiceFactory } from "../integration-auth/integration-auth-service"; import { deleteIntegrationSecrets } from "../integration-auth/integration-delete-secret"; import { TKmsServiceFactory } from "../kms/kms-service"; +import { KmsDataKey } from "../kms/kms-types"; import { TProjectBotServiceFactory } from "../project-bot/project-bot-service"; import { TSecretDALFactory } from "../secret/secret-dal"; import { TSecretQueueFactory } from "../secret/secret-queue"; @@ -237,6 +238,46 @@ export const integrationServiceFactory = ({ return { ...integration, envId: integration.environment.id }; }; + const getIntegrationAWSIamRole = async ({ id, actor, actorAuthMethod, actorId, actorOrgId }: TGetIntegrationDTO) => { + const integration = await integrationDAL.findById(id); + + if (!integration) { + throw new NotFoundError({ + message: `Integration with ID '${id}' not found` + }); + } + + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + integration?.projectId || "", + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); + + const integrationAuth = await integrationAuthDAL.findById(integration.integrationAuthId); + + const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId: integration.projectId + }); + let awsIamRole: string | null = null; + if (integrationAuth.encryptedAwsAssumeIamRoleArn) { + const awsAssumeRoleArn = secretManagerDecryptor({ + cipherTextBlob: Buffer.from(integrationAuth.encryptedAwsAssumeIamRoleArn) + }).toString(); + if (awsAssumeRoleArn) { + const [, role] = awsAssumeRoleArn.split(":role/"); + awsIamRole = role; + } + } + + return { + role: awsIamRole + }; + }; + const deleteIntegration = async ({ actorId, id, @@ -329,6 +370,7 @@ export const integrationServiceFactory = ({ deleteIntegration, listIntegrationByProject, getIntegration, + getIntegrationAWSIamRole, syncIntegration }; }; diff --git a/frontend/src/hooks/api/integrations/types.ts b/frontend/src/hooks/api/integrations/types.ts index bfaa73884..17e7265d6 100644 --- a/frontend/src/hooks/api/integrations/types.ts +++ b/frontend/src/hooks/api/integrations/types.ts @@ -57,6 +57,9 @@ export type TIntegration = { shouldMaskSecrets?: boolean; shouldProtectSecrets?: boolean; shouldEnableDelete?: boolean; + + awsIamRole?: string; + region?: string; }; }; diff --git a/frontend/src/views/IntegrationsPage/IntegrationDetailsPage/components/IntegrationSettingsSection.tsx b/frontend/src/views/IntegrationsPage/IntegrationDetailsPage/components/IntegrationSettingsSection.tsx index 653204455..ee30e3968 100644 --- a/frontend/src/views/IntegrationsPage/IntegrationDetailsPage/components/IntegrationSettingsSection.tsx +++ b/frontend/src/views/IntegrationsPage/IntegrationDetailsPage/components/IntegrationSettingsSection.tsx @@ -26,7 +26,9 @@ const metadataMappings: Record { From 2b977eeb3348cfe9db790a2778dde5f2ee53015a Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Sat, 23 Nov 2024 03:42:54 +0400 Subject: [PATCH 08/20] fix: improve project error handling --- backend/src/services/project/project-dal.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/backend/src/services/project/project-dal.ts b/backend/src/services/project/project-dal.ts index 4e7425326..e5e447145 100644 --- a/backend/src/services/project/project-dal.ts +++ b/backend/src/services/project/project-dal.ts @@ -191,6 +191,10 @@ export const projectDALFactory = (db: TDbClient) => { return project; } catch (error) { + if (error instanceof NotFoundError) { + throw error; + } + throw new DatabaseError({ error, name: "Find all projects" }); } }; @@ -240,6 +244,10 @@ export const projectDALFactory = (db: TDbClient) => { return project; } catch (error) { + if (error instanceof NotFoundError || error instanceof UnauthorizedError) { + throw error; + } + throw new DatabaseError({ error, name: "Find project by slug" }); } }; @@ -260,7 +268,7 @@ export const projectDALFactory = (db: TDbClient) => { } throw new BadRequestError({ message: "Invalid filter type" }); } catch (error) { - if (error instanceof BadRequestError) { + if (error instanceof BadRequestError || error instanceof NotFoundError || error instanceof UnauthorizedError) { throw error; } throw new DatabaseError({ error, name: `Failed to find project by ${filter.type}` }); From 089d6812fd20a39c6a6c85a95e49321a1e0c1b5d Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Mon, 25 Nov 2024 16:00:20 +0400 Subject: [PATCH 09/20] Update ldap-fns.ts --- backend/src/ee/services/ldap-config/ldap-fns.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/backend/src/ee/services/ldap-config/ldap-fns.ts b/backend/src/ee/services/ldap-config/ldap-fns.ts index 66d799583..99b0d8d9b 100644 --- a/backend/src/ee/services/ldap-config/ldap-fns.ts +++ b/backend/src/ee/services/ldap-config/ldap-fns.ts @@ -36,8 +36,7 @@ export const testLDAPConfig = async (ldapConfig: TLDAPConfig): Promise }); ldapClient.on("error", (err) => { - logger.error("LDAP client error:", err); - logger.error(err); + logger.error(err, "LDAP client error"); resolve(false); }); From 463eb0014e7a234e7c9bd2fabdd3812ee8e4daad Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Mon, 25 Nov 2024 20:17:50 +0400 Subject: [PATCH 10/20] fix(dynamic-secrets): renewal 500 error --- .../dynamic-secret/providers/aws-elasticache.ts | 2 +- .../services/dynamic-secret/providers/aws-iam.ts | 5 ++--- .../dynamic-secret/providers/azure-entra-id.ts | 10 +++++----- .../services/dynamic-secret/providers/cassandra.ts | 14 +++++++++----- .../dynamic-secret/providers/elastic-search.ts | 2 +- .../ee/services/dynamic-secret/providers/ldap.ts | 2 +- .../services/dynamic-secret/providers/mongo-db.ts | 1 + .../services/dynamic-secret/providers/rabbit-mq.ts | 2 +- .../ee/services/dynamic-secret/providers/redis.ts | 2 ++ .../services/dynamic-secret/providers/sap-hana.ts | 8 +++++--- .../services/dynamic-secret/providers/snowflake.ts | 9 ++++----- .../dynamic-secret/providers/sql-database.ts | 12 +++++++++--- .../ee/services/dynamic-secret/providers/totp.ts | 8 +++----- 13 files changed, 44 insertions(+), 33 deletions(-) diff --git a/backend/src/ee/services/dynamic-secret/providers/aws-elasticache.ts b/backend/src/ee/services/dynamic-secret/providers/aws-elasticache.ts index 2cb862029..46360976d 100644 --- a/backend/src/ee/services/dynamic-secret/providers/aws-elasticache.ts +++ b/backend/src/ee/services/dynamic-secret/providers/aws-elasticache.ts @@ -212,7 +212,7 @@ export const AwsElastiCacheDatabaseProvider = (): TDynamicProviderFns => { }; const renew = async (inputs: unknown, entityId: string) => { - // Do nothing + // No renewal necessary return { entityId }; }; diff --git a/backend/src/ee/services/dynamic-secret/providers/aws-iam.ts b/backend/src/ee/services/dynamic-secret/providers/aws-iam.ts index 3feafa534..a1aa780f7 100644 --- a/backend/src/ee/services/dynamic-secret/providers/aws-iam.ts +++ b/backend/src/ee/services/dynamic-secret/providers/aws-iam.ts @@ -179,9 +179,8 @@ export const AwsIamProvider = (): TDynamicProviderFns => { }; const renew = async (_inputs: unknown, entityId: string) => { - // do nothing - const username = entityId; - return { entityId: username }; + // No renewal necessary + return { entityId }; }; return { diff --git a/backend/src/ee/services/dynamic-secret/providers/azure-entra-id.ts b/backend/src/ee/services/dynamic-secret/providers/azure-entra-id.ts index e2dfe2d4b..88d333aad 100644 --- a/backend/src/ee/services/dynamic-secret/providers/azure-entra-id.ts +++ b/backend/src/ee/services/dynamic-secret/providers/azure-entra-id.ts @@ -55,11 +55,6 @@ export const AzureEntraIDProvider = (): TDynamicProviderFns & { return data.success; }; - const renew = async (inputs: unknown, entityId: string) => { - // Do nothing - return { entityId }; - }; - const create = async (inputs: unknown) => { const providerInputs = await validateProviderInputs(inputs); const data = await getToken(providerInputs.tenantId, providerInputs.applicationId, providerInputs.clientSecret); @@ -127,6 +122,11 @@ export const AzureEntraIDProvider = (): TDynamicProviderFns & { return users; }; + const renew = async (inputs: unknown, entityId: string) => { + // No renewal necessary + return { entityId }; + }; + return { validateProviderInputs, validateConnection, diff --git a/backend/src/ee/services/dynamic-secret/providers/cassandra.ts b/backend/src/ee/services/dynamic-secret/providers/cassandra.ts index aea0b9c99..20e93e927 100644 --- a/backend/src/ee/services/dynamic-secret/providers/cassandra.ts +++ b/backend/src/ee/services/dynamic-secret/providers/cassandra.ts @@ -99,20 +99,24 @@ export const CassandraProvider = (): TDynamicProviderFns => { const renew = async (inputs: unknown, entityId: string, expireAt: number) => { const providerInputs = await validateProviderInputs(inputs); + if (!providerInputs.renewStatement) return { entityId }; + const client = await getClient(providerInputs); - const username = entityId; const expiration = new Date(expireAt).toISOString(); const { keyspace } = providerInputs; - const renewStatement = handlebars.compile(providerInputs.revocationStatement)({ username, keyspace, expiration }); + const renewStatement = handlebars.compile(providerInputs.renewStatement)({ + username: entityId, + keyspace, + expiration + }); const queries = renewStatement.toString().split(";").filter(Boolean); - for (const query of queries) { - // eslint-disable-next-line + for await (const query of queries) { await client.execute(query); } await client.shutdown(); - return { entityId: username }; + return { entityId }; }; return { diff --git a/backend/src/ee/services/dynamic-secret/providers/elastic-search.ts b/backend/src/ee/services/dynamic-secret/providers/elastic-search.ts index bfe0ac443..18881834d 100644 --- a/backend/src/ee/services/dynamic-secret/providers/elastic-search.ts +++ b/backend/src/ee/services/dynamic-secret/providers/elastic-search.ts @@ -96,7 +96,7 @@ export const ElasticSearchProvider = (): TDynamicProviderFns => { }; const renew = async (inputs: unknown, entityId: string) => { - // Do nothing + // No renewal necessary return { entityId }; }; diff --git a/backend/src/ee/services/dynamic-secret/providers/ldap.ts b/backend/src/ee/services/dynamic-secret/providers/ldap.ts index f94e61629..fc1ef01d6 100644 --- a/backend/src/ee/services/dynamic-secret/providers/ldap.ts +++ b/backend/src/ee/services/dynamic-secret/providers/ldap.ts @@ -268,7 +268,7 @@ export const LdapProvider = (): TDynamicProviderFns => { }; const renew = async (inputs: unknown, entityId: string) => { - // Do nothing + // No renewal necessary return { entityId }; }; diff --git a/backend/src/ee/services/dynamic-secret/providers/mongo-db.ts b/backend/src/ee/services/dynamic-secret/providers/mongo-db.ts index b824f5aa8..a50af88bc 100644 --- a/backend/src/ee/services/dynamic-secret/providers/mongo-db.ts +++ b/backend/src/ee/services/dynamic-secret/providers/mongo-db.ts @@ -88,6 +88,7 @@ export const MongoDBProvider = (): TDynamicProviderFns => { }; const renew = async (_inputs: unknown, entityId: string) => { + // No renewal necessary return { entityId }; }; diff --git a/backend/src/ee/services/dynamic-secret/providers/rabbit-mq.ts b/backend/src/ee/services/dynamic-secret/providers/rabbit-mq.ts index 00d3b538f..e78fa3725 100644 --- a/backend/src/ee/services/dynamic-secret/providers/rabbit-mq.ts +++ b/backend/src/ee/services/dynamic-secret/providers/rabbit-mq.ts @@ -142,7 +142,7 @@ export const RabbitMqProvider = (): TDynamicProviderFns => { }; const renew = async (inputs: unknown, entityId: string) => { - // Do nothing + // No renewal necessary return { entityId }; }; diff --git a/backend/src/ee/services/dynamic-secret/providers/redis.ts b/backend/src/ee/services/dynamic-secret/providers/redis.ts index 0e7ae99a0..b08c5d421 100644 --- a/backend/src/ee/services/dynamic-secret/providers/redis.ts +++ b/backend/src/ee/services/dynamic-secret/providers/redis.ts @@ -141,6 +141,8 @@ export const RedisDatabaseProvider = (): TDynamicProviderFns => { const renew = async (inputs: unknown, entityId: string, expireAt: number) => { const providerInputs = await validateProviderInputs(inputs); + if (!providerInputs.renewStatement) return { entityId }; + const connection = await getClient(providerInputs); const username = entityId; diff --git a/backend/src/ee/services/dynamic-secret/providers/sap-hana.ts b/backend/src/ee/services/dynamic-secret/providers/sap-hana.ts index d120cf4fe..388994fb4 100644 --- a/backend/src/ee/services/dynamic-secret/providers/sap-hana.ts +++ b/backend/src/ee/services/dynamic-secret/providers/sap-hana.ts @@ -135,13 +135,15 @@ export const SapHanaProvider = (): TDynamicProviderFns => { return { entityId: username }; }; - const renew = async (inputs: unknown, username: string, expireAt: number) => { + const renew = async (inputs: unknown, entityId: string, expireAt: number) => { const providerInputs = await validateProviderInputs(inputs); + if (!providerInputs.renewStatement) return { entityId }; + const client = await getClient(providerInputs); try { const expiration = new Date(expireAt).toISOString(); - const renewStatement = handlebars.compile(providerInputs.renewStatement)({ username, expiration }); + const renewStatement = handlebars.compile(providerInputs.renewStatement)({ username: entityId, expiration }); const queries = renewStatement.toString().split(";").filter(Boolean); for await (const query of queries) { await new Promise((resolve, reject) => { @@ -161,7 +163,7 @@ export const SapHanaProvider = (): TDynamicProviderFns => { client.disconnect(); } - return { entityId: username }; + return { entityId }; }; return { diff --git a/backend/src/ee/services/dynamic-secret/providers/snowflake.ts b/backend/src/ee/services/dynamic-secret/providers/snowflake.ts index 27ac3f49c..26e7a590a 100644 --- a/backend/src/ee/services/dynamic-secret/providers/snowflake.ts +++ b/backend/src/ee/services/dynamic-secret/providers/snowflake.ts @@ -131,17 +131,16 @@ export const SnowflakeProvider = (): TDynamicProviderFns => { return { entityId: username }; }; - const renew = async (inputs: unknown, username: string, expireAt: number) => { + const renew = async (inputs: unknown, entityId: string, expireAt: number) => { const providerInputs = await validateProviderInputs(inputs); - - if (!providerInputs.renewStatement) return { entityId: username }; + if (!providerInputs.renewStatement) return { entityId }; const client = await getClient(providerInputs); try { const expiration = getDaysToExpiry(new Date(expireAt)); const renewStatement = handlebars.compile(providerInputs.renewStatement)({ - username, + username: entityId, expiration }); @@ -161,7 +160,7 @@ export const SnowflakeProvider = (): TDynamicProviderFns => { client.destroy(noop); } - return { entityId: username }; + return { entityId }; }; return { diff --git a/backend/src/ee/services/dynamic-secret/providers/sql-database.ts b/backend/src/ee/services/dynamic-secret/providers/sql-database.ts index 6acf23b06..511017586 100644 --- a/backend/src/ee/services/dynamic-secret/providers/sql-database.ts +++ b/backend/src/ee/services/dynamic-secret/providers/sql-database.ts @@ -110,13 +110,19 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => { const renew = async (inputs: unknown, entityId: string, expireAt: number) => { const providerInputs = await validateProviderInputs(inputs); + if (!providerInputs.renewStatement) return { entityId }; + const db = await getClient(providerInputs); - const username = entityId; const expiration = new Date(expireAt).toISOString(); const { database } = providerInputs; - const renewStatement = handlebars.compile(providerInputs.renewStatement)({ username, expiration, database }); + const renewStatement = handlebars.compile(providerInputs.renewStatement)({ + username: entityId, + expiration, + database + }); + if (renewStatement) { const queries = renewStatement.toString().split(";").filter(Boolean); await db.transaction(async (tx) => { @@ -128,7 +134,7 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => { } await db.destroy(); - return { entityId: username }; + return { entityId }; }; return { diff --git a/backend/src/ee/services/dynamic-secret/providers/totp.ts b/backend/src/ee/services/dynamic-secret/providers/totp.ts index 4e3ab6eb2..d16b82306 100644 --- a/backend/src/ee/services/dynamic-secret/providers/totp.ts +++ b/backend/src/ee/services/dynamic-secret/providers/totp.ts @@ -1,7 +1,6 @@ import { authenticator } from "otplib"; import { HashAlgorithms } from "otplib/core"; -import { BadRequestError } from "@app/lib/errors"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { DynamicSecretTotpSchema, TDynamicProviderFns, TotpConfigType } from "./models"; @@ -76,10 +75,9 @@ export const TotpProvider = (): TDynamicProviderFns => { }; // eslint-disable-next-line @typescript-eslint/no-unused-vars - const renew = async (_inputs: unknown, _entityId: string) => { - throw new BadRequestError({ - message: "Lease renewal is not supported for TOTPs" - }); + const renew = async (_inputs: unknown, entityId: string) => { + // No renewal necessary + return { entityId }; }; return { From f034adba76597dff665890a0f70f05a6a672cb5b Mon Sep 17 00:00:00 2001 From: = Date: Mon, 25 Nov 2024 22:22:54 +0530 Subject: [PATCH 11/20] feat: resolved saml failing when signup is disabled --- backend/src/server/routes/v3/signup-router.ts | 7 ------- backend/src/services/auth/auth-signup-service.ts | 11 ++++++++++- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/backend/src/server/routes/v3/signup-router.ts b/backend/src/server/routes/v3/signup-router.ts index d801e85ef..e95254816 100644 --- a/backend/src/server/routes/v3/signup-router.ts +++ b/backend/src/server/routes/v3/signup-router.ts @@ -119,13 +119,6 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => { if (!userAgent) throw new Error("user agent header is required"); const appCfg = getConfig(); - const serverCfg = await getServerCfg(); - if (!serverCfg.allowSignUp) { - throw new ForbiddenRequestError({ - message: "Signup's are disabled" - }); - } - const { user, accessToken, refreshToken, organizationId } = await server.services.signup.completeEmailAccountSignup({ ...req.body, diff --git a/backend/src/services/auth/auth-signup-service.ts b/backend/src/services/auth/auth-signup-service.ts index b55d01308..a652c2a5b 100644 --- a/backend/src/services/auth/auth-signup-service.ts +++ b/backend/src/services/auth/auth-signup-service.ts @@ -9,7 +9,7 @@ import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns"; import { getConfig } from "@app/lib/config/env"; import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; import { generateUserSrpKeys, getUserPrivateKey } from "@app/lib/crypto/srp"; -import { NotFoundError } from "@app/lib/errors"; +import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { isDisposableEmail } from "@app/lib/validator"; import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal"; @@ -23,6 +23,7 @@ import { TOrgServiceFactory } from "../org/org-service"; import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal"; import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal"; import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; +import { getServerCfg } from "../super-admin/super-admin-service"; import { TUserDALFactory } from "../user/user-dal"; import { UserEncryption } from "../user/user-types"; import { TAuthDALFactory } from "./auth-dal"; @@ -151,6 +152,8 @@ export const authSignupServiceFactory = ({ authorization }: TCompleteAccountSignupDTO) => { const appCfg = getConfig(); + const serverCfg = await getServerCfg(); + const user = await userDAL.findOne({ username: email }); if (!user || (user && user.isAccepted)) { throw new Error("Failed to complete account for complete user"); @@ -163,6 +166,12 @@ export const authSignupServiceFactory = ({ authMethod = userAuthMethod; organizationId = orgId; } else { + // disallow signup if disabled. we are not doing this for providerAuthToken because we allow signups via saml or sso + if (!serverCfg.allowSignUp) { + throw new ForbiddenRequestError({ + message: "Signup's are disabled" + }); + } validateSignUpAuthorization(authorization, user.id); } From 32430a6a1655f6082f6946e7534249f924d32ff3 Mon Sep 17 00:00:00 2001 From: McPizza Date: Mon, 25 Nov 2024 21:59:14 +0100 Subject: [PATCH 12/20] feat: Add Project Descriptions (#2774) * feat: :sparkles: initial backend project description --- .../20241119143026_add-project-descripton.ts | 23 ++ backend/src/db/schemas/kms-root-config.ts | 2 +- backend/src/db/schemas/projects.ts | 3 +- backend/src/lib/api-docs/constants.ts | 2 + backend/src/server/routes/sanitizedSchemas.ts | 1 + .../src/server/routes/v1/project-router.ts | 7 + .../src/server/routes/v2/project-router.ts | 8 +- .../src/services/project/project-service.ts | 3 + backend/src/services/project/project-types.ts | 2 + .../v2/projects/NewProjectModal.tsx | 328 ++++++++++++++++++ frontend/src/components/v2/projects/index.tsx | 1 + frontend/src/hooks/api/types.ts | 2 +- frontend/src/hooks/api/workspace/index.tsx | 5 +- frontend/src/hooks/api/workspace/queries.tsx | 24 +- frontend/src/hooks/api/workspace/types.ts | 10 +- frontend/src/layouts/AppLayout/AppLayout.tsx | 279 +-------------- .../src/pages/org/[id]/overview/index.tsx | 321 ++--------------- .../ProjectGeneralTab/ProjectGeneralTab.tsx | 4 +- .../ProjectNameChangeSection.tsx | 119 ------- .../ProjectNameChangeSection/index.tsx | 1 - .../CopyButton.tsx | 0 .../ProjectOverviewChangeSection.tsx | 168 +++++++++ .../ProjectOverviewChangeSection/index.tsx | 1 + .../ProjectSettingsPage/components/index.tsx | 2 +- 24 files changed, 615 insertions(+), 701 deletions(-) create mode 100644 backend/src/db/migrations/20241119143026_add-project-descripton.ts create mode 100644 frontend/src/components/v2/projects/NewProjectModal.tsx create mode 100644 frontend/src/components/v2/projects/index.tsx delete mode 100644 frontend/src/views/Settings/ProjectSettingsPage/components/ProjectNameChangeSection/ProjectNameChangeSection.tsx delete mode 100644 frontend/src/views/Settings/ProjectSettingsPage/components/ProjectNameChangeSection/index.tsx rename frontend/src/views/Settings/ProjectSettingsPage/components/{ProjectNameChangeSection => ProjectOverviewChangeSection}/CopyButton.tsx (100%) create mode 100644 frontend/src/views/Settings/ProjectSettingsPage/components/ProjectOverviewChangeSection/ProjectOverviewChangeSection.tsx create mode 100644 frontend/src/views/Settings/ProjectSettingsPage/components/ProjectOverviewChangeSection/index.tsx diff --git a/backend/src/db/migrations/20241119143026_add-project-descripton.ts b/backend/src/db/migrations/20241119143026_add-project-descripton.ts new file mode 100644 index 000000000..3c78c99e2 --- /dev/null +++ b/backend/src/db/migrations/20241119143026_add-project-descripton.ts @@ -0,0 +1,23 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const hasProjectDescription = await knex.schema.hasColumn(TableName.Project, "description"); + + if (!hasProjectDescription) { + await knex.schema.alterTable(TableName.Project, (t) => { + t.string("description"); + }); + } +} + +export async function down(knex: Knex): Promise { + const hasProjectDescription = await knex.schema.hasColumn(TableName.Project, "description"); + + if (hasProjectDescription) { + await knex.schema.alterTable(TableName.Project, (t) => { + t.dropColumn("description"); + }); + } +} diff --git a/backend/src/db/schemas/kms-root-config.ts b/backend/src/db/schemas/kms-root-config.ts index d15e1dff8..c9c1ebda5 100644 --- a/backend/src/db/schemas/kms-root-config.ts +++ b/backend/src/db/schemas/kms-root-config.ts @@ -12,7 +12,7 @@ import { TImmutableDBKeys } from "./models"; export const KmsRootConfigSchema = z.object({ id: z.string().uuid(), encryptedRootKey: zodBuffer, - encryptionStrategy: z.string(), + encryptionStrategy: z.string().default("SOFTWARE").nullable().optional(), createdAt: z.date(), updatedAt: z.date() }); diff --git a/backend/src/db/schemas/projects.ts b/backend/src/db/schemas/projects.ts index deba51b9a..5c5f9774b 100644 --- a/backend/src/db/schemas/projects.ts +++ b/backend/src/db/schemas/projects.ts @@ -23,7 +23,8 @@ export const ProjectsSchema = z.object({ kmsCertificateKeyId: z.string().uuid().nullable().optional(), auditLogsRetentionDays: z.number().nullable().optional(), kmsSecretManagerKeyId: z.string().uuid().nullable().optional(), - kmsSecretManagerEncryptedDataKey: zodBuffer.nullable().optional() + kmsSecretManagerEncryptedDataKey: zodBuffer.nullable().optional(), + description: z.string().nullable().optional() }); export type TProjects = z.infer; diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 136a4db29..9d0382b5c 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -391,6 +391,7 @@ export const PROJECTS = { CREATE: { organizationSlug: "The slug of the organization to create the project in.", projectName: "The name of the project to create.", + projectDescription: "An optional description label for the project.", slug: "An optional slug for the project.", template: "The name of the project template, if specified, to apply to this project." }, @@ -403,6 +404,7 @@ export const PROJECTS = { UPDATE: { workspaceId: "The ID of the project to update.", name: "The new name of the project.", + projectDescription: "An optional description label for the project.", autoCapitalization: "Disable or enable auto-capitalization for the project." }, GET_KEY: { diff --git a/backend/src/server/routes/sanitizedSchemas.ts b/backend/src/server/routes/sanitizedSchemas.ts index bbbe57631..3fbbc60e3 100644 --- a/backend/src/server/routes/sanitizedSchemas.ts +++ b/backend/src/server/routes/sanitizedSchemas.ts @@ -212,6 +212,7 @@ export const SanitizedAuditLogStreamSchema = z.object({ export const SanitizedProjectSchema = ProjectsSchema.pick({ id: true, name: true, + description: true, slug: true, autoCapitalization: true, orgId: true, diff --git a/backend/src/server/routes/v1/project-router.ts b/backend/src/server/routes/v1/project-router.ts index e5e2f636c..f27462d02 100644 --- a/backend/src/server/routes/v1/project-router.ts +++ b/backend/src/server/routes/v1/project-router.ts @@ -296,6 +296,12 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { .max(64, { message: "Name must be 64 or fewer characters" }) .optional() .describe(PROJECTS.UPDATE.name), + description: z + .string() + .trim() + .max(256, { message: "Description must be 256 or fewer characters" }) + .optional() + .describe(PROJECTS.UPDATE.projectDescription), autoCapitalization: z.boolean().optional().describe(PROJECTS.UPDATE.autoCapitalization) }), response: { @@ -313,6 +319,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { }, update: { name: req.body.name, + description: req.body.description, autoCapitalization: req.body.autoCapitalization }, actorAuthMethod: req.permission.authMethod, diff --git a/backend/src/server/routes/v2/project-router.ts b/backend/src/server/routes/v2/project-router.ts index c2aa446b4..0e271eb0e 100644 --- a/backend/src/server/routes/v2/project-router.ts +++ b/backend/src/server/routes/v2/project-router.ts @@ -161,6 +161,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { ], body: z.object({ projectName: z.string().trim().describe(PROJECTS.CREATE.projectName), + projectDescription: z.string().trim().optional().describe(PROJECTS.CREATE.projectDescription), slug: z .string() .min(5) @@ -194,6 +195,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { actorOrgId: req.permission.orgId, actorAuthMethod: req.permission.authMethod, workspaceName: req.body.projectName, + workspaceDescription: req.body.projectDescription, slug: req.body.slug, kmsKeyId: req.body.kmsKeyId, template: req.body.template @@ -312,8 +314,9 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { slug: slugSchema.describe("The slug of the project to update.") }), body: z.object({ - name: z.string().trim().optional().describe("The new name of the project."), - autoCapitalization: z.boolean().optional().describe("The new auto-capitalization setting.") + name: z.string().trim().optional().describe(PROJECTS.UPDATE.name), + description: z.string().trim().optional().describe(PROJECTS.UPDATE.projectDescription), + autoCapitalization: z.boolean().optional().describe(PROJECTS.UPDATE.autoCapitalization) }), response: { 200: SanitizedProjectSchema @@ -330,6 +333,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { }, update: { name: req.body.name, + description: req.body.description, autoCapitalization: req.body.autoCapitalization }, actorId: req.permission.id, diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts index dfe2ce3ec..53e934716 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -149,6 +149,7 @@ export const projectServiceFactory = ({ actorOrgId, actorAuthMethod, workspaceName, + workspaceDescription, slug: projectSlug, kmsKeyId, tx: trx, @@ -206,6 +207,7 @@ export const projectServiceFactory = ({ const project = await projectDAL.create( { name: workspaceName, + description: workspaceDescription, orgId: organization.id, slug: projectSlug || slugify(`${workspaceName}-${alphaNumericNanoId(4)}`), kmsSecretManagerKeyId: kmsKeyId, @@ -496,6 +498,7 @@ export const projectServiceFactory = ({ const updatedProject = await projectDAL.updateById(project.id, { name: update.name, + description: update.description, autoCapitalization: update.autoCapitalization }); return updatedProject; diff --git a/backend/src/services/project/project-types.ts b/backend/src/services/project/project-types.ts index 28cda2d95..b826f2a6a 100644 --- a/backend/src/services/project/project-types.ts +++ b/backend/src/services/project/project-types.ts @@ -29,6 +29,7 @@ export type TCreateProjectDTO = { actorId: string; actorOrgId?: string; workspaceName: string; + workspaceDescription?: string; slug?: string; kmsKeyId?: string; createDefaultEnvs?: boolean; @@ -69,6 +70,7 @@ export type TUpdateProjectDTO = { filter: Filter; update: { name?: string; + description?: string; autoCapitalization?: boolean; }; } & Omit; diff --git a/frontend/src/components/v2/projects/NewProjectModal.tsx b/frontend/src/components/v2/projects/NewProjectModal.tsx new file mode 100644 index 000000000..8f2cf79e8 --- /dev/null +++ b/frontend/src/components/v2/projects/NewProjectModal.tsx @@ -0,0 +1,328 @@ +import { FC, useEffect } from "react"; +import { Controller, useForm } from "react-hook-form"; +import { useRouter } from "next/router"; +import { faInfoCircle } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { zodResolver } from "@hookform/resolvers/zod"; +import z from "zod"; + +import { createNotification } from "@app/components/notifications"; +import { OrgPermissionCan } from "@app/components/permissions"; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, + Button, + Checkbox, + FormControl, + Input, + Modal, + ModalClose, + ModalContent, + Select, + SelectItem, + TextArea +} from "@app/components/v2"; +import { + OrgPermissionActions, + OrgPermissionSubjects, + useOrganization, + useOrgPermission, + useSubscription, + useUser +} from "@app/context"; +import { + fetchOrgUsers, + useAddUserToWsNonE2EE, + useCreateWorkspace, + useGetExternalKmsList +} from "@app/hooks/api"; +import { INTERNAL_KMS_KEY_ID } from "@app/hooks/api/kms/types"; +import { InfisicalProjectTemplate, useListProjectTemplates } from "@app/hooks/api/projectTemplates"; + +const formSchema = z.object({ + name: z.string().trim().min(1, "Required").max(64, "Too long, maximum length is 64 characters"), + description: z + .string() + .trim() + .max(256, "Description too long, max length is 256 characters") + .optional(), + addMembers: z.boolean(), + kmsKeyId: z.string(), + template: z.string() +}); + +type TAddProjectFormData = z.infer; + +interface NewProjectModalProps { + isOpen: boolean; + onOpenChange: (isOpen: boolean) => void; +} + +type NewProjectFormProps = Pick; + +const NewProjectForm = ({ onOpenChange }: NewProjectFormProps) => { + const router = useRouter(); + const { currentOrg } = useOrganization(); + const { permission } = useOrgPermission(); + const { user } = useUser(); + const createWs = useCreateWorkspace(); + const addUsersToProject = useAddUserToWsNonE2EE(); + const { subscription } = useSubscription(); + + const canReadProjectTemplates = permission.can( + OrgPermissionActions.Read, + OrgPermissionSubjects.ProjectTemplates + ); + + const { data: projectTemplates = [] } = useListProjectTemplates({ + enabled: Boolean(canReadProjectTemplates && subscription?.projectTemplates) + }); + + const { data: externalKmsList } = useGetExternalKmsList(currentOrg?.id!, { + enabled: permission.can(OrgPermissionActions.Read, OrgPermissionSubjects.Kms) + }); + + const { + control, + handleSubmit, + reset, + formState: { isSubmitting, errors } + } = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + kmsKeyId: INTERNAL_KMS_KEY_ID, + template: InfisicalProjectTemplate.Default + } + }); + + useEffect(() => { + if (Object.keys(errors).length > 0) { + console.log("Current form errors:", errors); + } + }, [errors]); + + const onCreateProject = async ({ + name, + description, + addMembers, + kmsKeyId, + template + }: TAddProjectFormData) => { + // type check + if (!currentOrg) return; + if (!user) return; + try { + const { + data: { + project: { id: newProjectId } + } + } = await createWs.mutateAsync({ + projectName: name, + projectDescription: description, + kmsKeyId: kmsKeyId !== INTERNAL_KMS_KEY_ID ? kmsKeyId : undefined, + template + }); + + if (addMembers) { + const orgUsers = await fetchOrgUsers(currentOrg.id); + await addUsersToProject.mutateAsync({ + usernames: orgUsers + .filter( + (member) => member.user.username !== user.username && member.status === "accepted" + ) + .map((member) => member.user.username), + projectId: newProjectId, + orgId: currentOrg.id + }); + } + // eslint-disable-next-line no-promise-executor-return -- We do this because the function returns too fast, which sometimes causes an error when the user is redirected. + await new Promise((resolve) => setTimeout(resolve, 2_000)); + + createNotification({ text: "Project created", type: "success" }); + reset(); + onOpenChange(false); + router.push(`/project/${newProjectId}/secrets/overview`); + } catch (err) { + console.error(err); + createNotification({ text: "Failed to create project", type: "error" }); + } + }; + const onSubmit = handleSubmit((data) => { + return onCreateProject(data); + }); + return ( +
+
+ ( + + + + )} + /> + ( + +