diff --git a/backend/spec.json b/backend/spec.json index 013b5fd4d..1afbc8dce 100644 --- a/backend/spec.json +++ b/backend/spec.json @@ -3203,6 +3203,9 @@ "name": { "example": "any" }, + "tagColor": { + "example": "any" + }, "slug": { "example": "any" } diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index 3a3405e3d..b5e50d9da 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -37,6 +37,7 @@ export const getClientIdNetlify = async () => (await client.getSecret("CLIENT_ID export const getClientIdGitHub = async () => (await client.getSecret("CLIENT_ID_GITHUB")).secretValue; export const getClientIdGitLab = async () => (await client.getSecret("CLIENT_ID_GITLAB")).secretValue; export const getClientIdBitBucket = async () => (await client.getSecret("CLIENT_ID_BITBUCKET")).secretValue; +export const getClientIdGCPSecretManager = async () => (await client.getSecret("CLIENT_ID_GCP_SECRET_MANAGER")).secretValue; export const getClientSecretAzure = async () => (await client.getSecret("CLIENT_SECRET_AZURE")).secretValue; export const getClientSecretHeroku = async () => (await client.getSecret("CLIENT_SECRET_HEROKU")).secretValue; export const getClientSecretVercel = async () => (await client.getSecret("CLIENT_SECRET_VERCEL")).secretValue; @@ -44,6 +45,7 @@ export const getClientSecretNetlify = async () => (await client.getSecret("CLIEN export const getClientSecretGitHub = async () => (await client.getSecret("CLIENT_SECRET_GITHUB")).secretValue; export const getClientSecretGitLab = async () => (await client.getSecret("CLIENT_SECRET_GITLAB")).secretValue; export const getClientSecretBitBucket = async () => (await client.getSecret("CLIENT_SECRET_BITBUCKET")).secretValue; +export const getClientSecretGCPSecretManager = async () => (await client.getSecret("CLIENT_SECRET_GCP_SECRET_MANAGER")).secretValue; export const getClientSlugVercel = async () => (await client.getSecret("CLIENT_SLUG_VERCEL")).secretValue; export const getClientIdGoogleLogin = async () => (await client.getSecret("CLIENT_ID_GOOGLE_LOGIN")).secretValue; diff --git a/backend/src/controllers/v1/authController.ts b/backend/src/controllers/v1/authController.ts index 03a9a7717..14c717e38 100644 --- a/backend/src/controllers/v1/authController.ts +++ b/backend/src/controllers/v1/authController.ts @@ -1,32 +1,20 @@ import { Request, Response } from "express"; -import fs from "fs"; -import path from "path"; import jwt from "jsonwebtoken"; import * as bigintConversion from "bigint-conversion"; // eslint-disable-next-line @typescript-eslint/no-var-requires const jsrp = require("jsrp"); -import { - LoginSRPDetail, - TokenVersion, - User, -} from "../../models"; +import { LoginSRPDetail, TokenVersion, User } from "../../models"; import { clearTokens, createToken, issueAuthTokens } from "../../helpers/auth"; import { checkUserDevice } from "../../helpers/user"; -import { - ACTION_LOGIN, - ACTION_LOGOUT, -} from "../../variables"; -import { - BadRequestError, - UnauthorizedRequestError, -} from "../../utils/errors"; +import { ACTION_LOGIN, ACTION_LOGOUT } from "../../variables"; +import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors"; import { EELogService } from "../../ee/services"; import { getUserAgentType } from "../../utils/posthog"; import { getHttpsEnabled, getJwtAuthLifetime, getJwtAuthSecret, - getJwtRefreshSecret, + getJwtRefreshSecret } from "../../config"; import { ActorType } from "../../ee/models"; @@ -44,13 +32,10 @@ declare module "jsonwebtoken" { * @returns */ export const login1 = async (req: Request, res: Response) => { - const { - email, - clientPublicKey, - }: { email: string; clientPublicKey: string } = req.body; + const { email, clientPublicKey }: { email: string; clientPublicKey: string } = req.body; const user = await User.findOne({ - email, + email }).select("+salt +verifier"); if (!user) throw new Error("Failed to find user"); @@ -59,21 +44,25 @@ export const login1 = async (req: Request, res: Response) => { server.init( { salt: user.salt, - verifier: user.verifier, + verifier: user.verifier }, async () => { // generate server-side public key const serverPublicKey = server.getPublicKey(); - await LoginSRPDetail.findOneAndReplace({ email: email }, { - email: email, - clientPublicKey: clientPublicKey, - serverBInt: bigintConversion.bigintToBuf(server.bInt), - }, { upsert: true, returnNewDocument: false }) + await LoginSRPDetail.findOneAndReplace( + { email: email }, + { + email: email, + clientPublicKey: clientPublicKey, + serverBInt: bigintConversion.bigintToBuf(server.bInt) + }, + { upsert: true, returnNewDocument: false } + ); return res.status(200).send({ serverPublicKey, - salt: user.salt, + salt: user.salt }); } ); @@ -89,15 +78,19 @@ export const login1 = async (req: Request, res: Response) => { export const login2 = async (req: Request, res: Response) => { const { email, clientProof } = req.body; const user = await User.findOne({ - email, + email }).select("+salt +verifier +publicKey +encryptedPrivateKey +iv +tag"); if (!user) throw new Error("Failed to find user"); - const loginSRPDetailFromDB = await LoginSRPDetail.findOneAndDelete({ email: email }) + const loginSRPDetailFromDB = await LoginSRPDetail.findOneAndDelete({ email: email }); if (!loginSRPDetailFromDB) { - return BadRequestError(Error("It looks like some details from the first login are not found. Please try login one again")) + return BadRequestError( + Error( + "It looks like some details from the first login are not found. Please try login one again" + ) + ); } const server = new jsrp.server(); @@ -105,7 +98,7 @@ export const login2 = async (req: Request, res: Response) => { { salt: user.salt, verifier: user.verifier, - b: loginSRPDetailFromDB.serverBInt, + b: loginSRPDetailFromDB.serverBInt }, async () => { server.setClientPublicKey(loginSRPDetailFromDB.clientPublicKey); @@ -117,13 +110,13 @@ export const login2 = async (req: Request, res: Response) => { await checkUserDevice({ user, ip: req.realIP, - userAgent: req.headers["user-agent"] ?? "", + userAgent: req.headers["user-agent"] ?? "" }); - const tokens = await issueAuthTokens({ + const tokens = await issueAuthTokens({ userId: user._id, ip: req.realIP, - userAgent: req.headers["user-agent"] ?? "", + userAgent: req.headers["user-agent"] ?? "" }); // store (refresh) token in httpOnly cookie @@ -131,20 +124,21 @@ export const login2 = async (req: Request, res: Response) => { httpOnly: true, path: "/", sameSite: "strict", - secure: await getHttpsEnabled(), + secure: await getHttpsEnabled() }); const loginAction = await EELogService.createAction({ name: ACTION_LOGIN, - userId: user._id, + userId: user._id }); - loginAction && await EELogService.createLog({ - userId: user._id, - actions: [loginAction], - channel: getUserAgentType(req.headers["user-agent"]), - ipAddress: req.realIP, - }); + loginAction && + (await EELogService.createLog({ + userId: user._id, + actions: [loginAction], + channel: getUserAgentType(req.headers["user-agent"]), + ipAddress: req.realIP + })); // return (access) token in response return res.status(200).send({ @@ -152,12 +146,12 @@ export const login2 = async (req: Request, res: Response) => { publicKey: user.publicKey, encryptedPrivateKey: user.encryptedPrivateKey, iv: user.iv, - tag: user.tag, + tag: user.tag }); } return res.status(400).send({ - message: "Failed to authenticate. Try again?", + message: "Failed to authenticate. Try again?" }); } ); @@ -171,7 +165,7 @@ export const login2 = async (req: Request, res: Response) => { */ export const logout = async (req: Request, res: Response) => { if (req.authData.actor.type === ActorType.USER && req.authData.tokenVersionId) { - await clearTokens(req.authData.tokenVersionId) + await clearTokens(req.authData.tokenVersionId); } // clear httpOnly cookie @@ -179,49 +173,44 @@ export const logout = async (req: Request, res: Response) => { httpOnly: true, path: "/", sameSite: "strict", - secure: (await getHttpsEnabled()) as boolean, + secure: (await getHttpsEnabled()) as boolean }); const logoutAction = await EELogService.createAction({ name: ACTION_LOGOUT, - userId: req.user._id, + userId: req.user._id }); - logoutAction && await EELogService.createLog({ - userId: req.user._id, - actions: [logoutAction], - channel: getUserAgentType(req.headers["user-agent"]), - ipAddress: req.realIP, - }); + logoutAction && + (await EELogService.createLog({ + userId: req.user._id, + actions: [logoutAction], + channel: getUserAgentType(req.headers["user-agent"]), + ipAddress: req.realIP + })); return res.status(200).send({ - message: "Successfully logged out.", + message: "Successfully logged out." }); }; -export const getCommonPasswords = async (req: Request, res: Response) => { - const commonPasswords = fs.readFileSync( - path.resolve(__dirname, "../../data/" + "common_passwords.txt"), - "utf8" - ).split("\n"); - - return res.status(200).send(commonPasswords); -} - export const revokeAllSessions = async (req: Request, res: Response) => { - await TokenVersion.updateMany({ - user: req.user._id, - }, { - $inc: { - refreshVersion: 1, - accessVersion: 1, + await TokenVersion.updateMany( + { + user: req.user._id }, - }); + { + $inc: { + refreshVersion: 1, + accessVersion: 1 + } + } + ); return res.status(200).send({ - message: "Successfully revoked all sessions.", - }); -} + message: "Successfully revoked all sessions." + }); +}; /** * Return user is authenticated @@ -231,9 +220,9 @@ export const revokeAllSessions = async (req: Request, res: Response) => { */ export const checkAuth = async (req: Request, res: Response) => { return res.status(200).send({ - message: "Authenticated", + message: "Authenticated" }); -} +}; /** * Return new JWT access token by first validating the refresh token @@ -244,47 +233,47 @@ export const checkAuth = async (req: Request, res: Response) => { export const getNewToken = async (req: Request, res: Response) => { const refreshToken = req.cookies.jid; - if (!refreshToken) throw BadRequestError({ - message: "Failed to find refresh token in request cookies" - }); + if (!refreshToken) + throw BadRequestError({ + message: "Failed to find refresh token in request cookies" + }); - const decodedToken = ( - jwt.verify(refreshToken, await getJwtRefreshSecret()) - ); + const decodedToken = jwt.verify(refreshToken, await getJwtRefreshSecret()); const user = await User.findOne({ - _id: decodedToken.userId, + _id: decodedToken.userId }).select("+publicKey +refreshVersion +accessVersion"); if (!user) throw new Error("Failed to authenticate unfound user"); - if (!user?.publicKey) - throw new Error("Failed to authenticate not fully set up account"); - + if (!user?.publicKey) throw new Error("Failed to authenticate not fully set up account"); + const tokenVersion = await TokenVersion.findById(decodedToken.tokenVersionId); - if (!tokenVersion) throw UnauthorizedRequestError({ - message: "Failed to validate refresh token", - }); + if (!tokenVersion) + throw UnauthorizedRequestError({ + message: "Failed to validate refresh token" + }); - if (decodedToken.refreshVersion !== tokenVersion.refreshVersion) throw BadRequestError({ - message: "Failed to validate refresh token", - }); + if (decodedToken.refreshVersion !== tokenVersion.refreshVersion) + throw BadRequestError({ + message: "Failed to validate refresh token" + }); const token = createToken({ payload: { userId: decodedToken.userId, tokenVersionId: tokenVersion._id.toString(), - accessVersion: tokenVersion.refreshVersion, + accessVersion: tokenVersion.refreshVersion }, expiresIn: await getJwtAuthLifetime(), - secret: await getJwtAuthSecret(), + secret: await getJwtAuthSecret() }); return res.status(200).send({ - token, + token }); }; export const handleAuthProviderCallback = (req: Request, res: Response) => { res.redirect(`/login/provider/success?token=${encodeURIComponent(req.providerAuthToken)}`); -} +}; diff --git a/backend/src/controllers/v1/integrationAuthController.ts b/backend/src/controllers/v1/integrationAuthController.ts index f4c25d54b..b91319a2f 100644 --- a/backend/src/controllers/v1/integrationAuthController.ts +++ b/backend/src/controllers/v1/integrationAuthController.ts @@ -547,6 +547,57 @@ export const getIntegrationAuthNorthflankSecretGroups = async (req: Request, res }); } +/** + * Return list of build configs for TeamCity project with id [appId] + * @param req + * @param res + * @returns + */ +export const getIntegrationAuthTeamCityBuildConfigs = async (req: Request, res: Response) => { + const appId = req.query.appId as string; + + interface TeamCityBuildConfig { + id: string; + name: string; + projectName: string; + projectId: string; + href: string; + webUrl: string; + } + + interface GetTeamCityBuildConfigsRes { + count: number; + href: string; + buildType: TeamCityBuildConfig[]; + } + + + if (appId && appId !== "") { + const { data: { buildType } } = ( + await standardRequest.get(`${req.integrationAuth.url}/app/rest/buildTypes`, { + params: { + locator: `project:${appId}` + }, + headers: { + Authorization: `Bearer ${req.accessToken}`, + Accept: "application/json", + }, + }) + ); + + return res.status(200).send({ + buildConfigs: buildType.map((buildConfig) => ({ + name: buildConfig.name, + buildConfigId: buildConfig.id + })) + }); + } + + return res.status(200).send({ + buildConfigs: [] + }); +} + /** * Delete integration authorization with id [integrationAuthId] * @param req diff --git a/backend/src/controllers/v1/integrationController.ts b/backend/src/controllers/v1/integrationController.ts index e5f6e7393..84d00f6bb 100644 --- a/backend/src/controllers/v1/integrationController.ts +++ b/backend/src/controllers/v1/integrationController.ts @@ -1,9 +1,8 @@ import { Request, Response } from "express"; import { Types } from "mongoose"; -import { Integration } from "../../models"; +import { Folder, Integration } from "../../models"; import { EventService } from "../../services"; import { eventStartIntegration } from "../../events"; -import Folder from "../../models/folder"; import { getFolderByPath } from "../../services/FolderService"; import { BadRequestError } from "../../utils/errors"; import { EEAuditLogService } from "../../ee/services"; @@ -30,7 +29,8 @@ export const createIntegration = async (req: Request, res: Response) => { owner, path, region, - secretPath + secretPath, + metadata } = req.body; const folders = await Folder.findOne({ @@ -65,7 +65,8 @@ export const createIntegration = async (req: Request, res: Response) => { region, secretPath, integration: req.integrationAuth.integration, - integrationAuth: new Types.ObjectId(integrationAuthId) + integrationAuth: new Types.ObjectId(integrationAuthId), + metadata }).save(); if (integration) { diff --git a/backend/src/controllers/v1/secretImportController.ts b/backend/src/controllers/v1/secretImportController.ts index 76b4945bd..23be98096 100644 --- a/backend/src/controllers/v1/secretImportController.ts +++ b/backend/src/controllers/v1/secretImportController.ts @@ -1,8 +1,6 @@ import { Request, Response } from "express"; import { isValidScope, validateMembership } from "../../helpers"; -import { ServiceTokenData } from "../../models"; -import Folder from "../../models/folder"; -import SecretImport from "../../models/secretImports"; +import { Folder, SecretImport, ServiceTokenData } from "../../models"; import { getAllImportedSecrets } from "../../services/SecretImportService"; import { getFolderWithPathFromId } from "../../services/FolderService"; import { BadRequestError, ResourceNotFoundError,UnauthorizedRequestError } from "../../utils/errors"; diff --git a/backend/src/controllers/v1/secretsFolderController.ts b/backend/src/controllers/v1/secretsFolderController.ts index 46d4c3c1f..cbc642592 100644 --- a/backend/src/controllers/v1/secretsFolderController.ts +++ b/backend/src/controllers/v1/secretsFolderController.ts @@ -4,8 +4,7 @@ import { EventType, FolderVersion } from "../../ee/models"; import { EEAuditLogService, EESecretService } from "../../ee/services"; import { validateMembership } from "../../helpers/membership"; import { isValidScope } from "../../helpers/secrets"; -import { Secret, ServiceTokenData } from "../../models"; -import Folder from "../../models/folder"; +import { Folder, Secret, ServiceTokenData } from "../../models"; import { appendFolder, deleteFolderById, diff --git a/backend/src/controllers/v1/webhookController.ts b/backend/src/controllers/v1/webhookController.ts index a79b794cf..7e84aae87 100644 --- a/backend/src/controllers/v1/webhookController.ts +++ b/backend/src/controllers/v1/webhookController.ts @@ -2,7 +2,7 @@ import { Request, Response } from "express"; import { Types } from "mongoose"; import { client, getRootEncryptionKey } from "../../config"; import { validateMembership } from "../../helpers"; -import Webhook from "../../models/webhooks"; +import { Webhook } from "../../models"; import { getWebhookPayload, triggerWebhookRequest } from "../../services/WebhookService"; import { BadRequestError, ResourceNotFoundError } from "../../utils/errors"; import { EEAuditLogService } from "../../ee/services"; diff --git a/backend/src/controllers/v2/secretController.ts b/backend/src/controllers/v2/secretController.ts index 5581a2ac7..bd36d6ab4 100644 --- a/backend/src/controllers/v2/secretController.ts +++ b/backend/src/controllers/v2/secretController.ts @@ -1,6 +1,5 @@ import { Request, Response } from "express"; import mongoose, { Types } from "mongoose"; -import Secret, { ISecret } from "../../models/secret"; import { CreateSecretRequestBody, ModifySecretRequestBody, @@ -20,7 +19,7 @@ import { SECRET_SHARED } from "../../variables"; import { TelemetryService } from "../../services"; -import { User } from "../../models"; +import { ISecret, Secret, User } from "../../models"; import { AccountNotFoundError } from "../../utils/errors"; /** diff --git a/backend/src/controllers/v2/secretsController.ts b/backend/src/controllers/v2/secretsController.ts index c87b3756a..6c9b29203 100644 --- a/backend/src/controllers/v2/secretsController.ts +++ b/backend/src/controllers/v2/secretsController.ts @@ -1,6 +1,6 @@ import { Types } from "mongoose"; import { Request, Response } from "express"; -import { ISecret, Secret, ServiceTokenData } from "../../models"; +import { Folder, ISecret, Secret, ServiceTokenData, Tag } from "../../models"; import { AuditLog, EventType, IAction, SecretVersion } from "../../ee/models"; import { ACTION_ADD_SECRETS, @@ -9,6 +9,7 @@ import { ACTION_UPDATE_SECRETS, ALGORITHM_AES_256_GCM, ENCODING_SCHEME_UTF8, + K8_USER_AGENT_NAME, SECRET_PERSONAL } from "../../variables"; import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors"; @@ -23,10 +24,8 @@ import { userHasWorkspaceAccess, userHasWriteOnlyAbility } from "../../ee/helpers/checkMembershipPermissions"; -import Tag from "../../models/tag"; import _ from "lodash"; import { BatchSecret, BatchSecretRequest } from "../../types/secret"; -import Folder from "../../models/folder"; import { getFolderByPath, getFolderIdFromServiceToken, @@ -59,7 +58,7 @@ export const batchSecrets = async (req: Request, res: Response) => { let secretPath = req.body.secretPath as string; let folderId = req.body.folderId as string; - + const createSecrets: BatchSecret[] = []; const updateSecrets: BatchSecret[] = []; const deleteSecrets: { _id: Types.ObjectId, secretName: string; }[] = []; @@ -154,7 +153,7 @@ export const batchSecrets = async (req: Request, res: Response) => { }; }) }); - + const auditLogs = await Promise.all( createdSecrets.map((secret, index) => { return EEAuditLogService.createAuditLog( @@ -178,7 +177,7 @@ export const batchSecrets = async (req: Request, res: Response) => { ); await AuditLog.insertMany(auditLogs); - + const addAction = (await EELogService.createAction({ name: ACTION_ADD_SECRETS, userId: req.user?._id, @@ -234,6 +233,9 @@ export const batchSecrets = async (req: Request, res: Response) => { $inc: { version: 1 }, + $unset: { + "metadata.source": true as const + }, ...u, _id: new Types.ObjectId(u._id) } @@ -277,7 +279,7 @@ export const batchSecrets = async (req: Request, res: Response) => { $in: updateSecrets.map((u) => new Types.ObjectId(u._id)) } }); - + const auditLogs = await Promise.all( updateSecrets.map((secret) => { return EEAuditLogService.createAuditLog( @@ -329,26 +331,26 @@ export const batchSecrets = async (req: Request, res: Response) => { // handle delete secrets if (deleteSecrets.length > 0) { const deleteSecretIds: Types.ObjectId[] = deleteSecrets.map((s) => s._id); - + const deletedSecretsObj = (await Secret.find({ _id: { $in: deleteSecretIds } })) - .reduce( - (obj: any, secret: ISecret) => ({ - ...obj, - [secret._id.toString()]: secret - }), - {} - ); - + .reduce( + (obj: any, secret: ISecret) => ({ + ...obj, + [secret._id.toString()]: secret + }), + {} + ); + await Secret.deleteMany({ _id: { $in: deleteSecretIds } }); - + await EESecretService.markDeletedSecretVersions({ secretIds: deleteSecretIds }); @@ -949,7 +951,7 @@ export const getSecrets = async (req: Request, res: Response) => { channel, ipAddress: req.realIP })); - + await EEAuditLogService.createAuditLog( req.authData, { @@ -966,21 +968,36 @@ export const getSecrets = async (req: Request, res: Response) => { ); const postHogClient = await TelemetryService.getPostHogClient(); + + // reduce the number of events captured + let shouldRecordK8Event = false + if (req.authData.userAgent == K8_USER_AGENT_NAME) { + const randomNumber = Math.random(); + if (randomNumber > 0.9) { + shouldRecordK8Event = true + } + } + if (postHogClient) { - postHogClient.capture({ - event: "secrets pulled", - distinctId: await TelemetryService.getDistinctId({ - authData: req.authData - }), - properties: { - numberOfSecrets: secrets.length, - environment, - workspaceId, - channel, - folderId, - userAgent: req.headers?.["user-agent"] - } - }); + const shouldCapture = req.authData.userAgent !== K8_USER_AGENT_NAME || shouldRecordK8Event; + const approximateForNoneCapturedEvents = secrets.length * 10 + + if (shouldCapture) { + postHogClient.capture({ + event: "secrets pulled", + distinctId: await TelemetryService.getDistinctId({ + authData: req.authData + }), + properties: { + numberOfSecrets: shouldRecordK8Event ? approximateForNoneCapturedEvents : secrets.length, + environment, + workspaceId, + folderId, + channel: req.authData.userAgentType, + userAgent: req.authData.userAgent + } + }); + } } return res.status(200).send({ @@ -1087,10 +1104,10 @@ export const updateSecrets = async (req: Request, res: Response) => { tags, ...(secretCommentCiphertext !== undefined && secretCommentIV && secretCommentTag ? { - secretCommentCiphertext, - secretCommentIV, - secretCommentTag - } + secretCommentCiphertext, + secretCommentIV, + secretCommentTag + } : {}) } } diff --git a/backend/src/controllers/v2/tagController.ts b/backend/src/controllers/v2/tagController.ts index 0d945c3e5..4a7e68bb7 100644 --- a/backend/src/controllers/v2/tagController.ts +++ b/backend/src/controllers/v2/tagController.ts @@ -1,15 +1,15 @@ import { Request, Response } from "express"; import { Types } from "mongoose"; -import { Membership, Secret } from "../../models"; -import Tag from "../../models/tag"; +import { Membership, Secret, Tag } from "../../models"; import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors"; export const createWorkspaceTag = async (req: Request, res: Response) => { const { workspaceId } = req.params; - const { name, slug } = req.body; + const { name, slug, tagColor } = req.body; const tagToCreate = { name, + tagColor, workspace: new Types.ObjectId(workspaceId), slug, user: new Types.ObjectId(req.user._id), diff --git a/backend/src/controllers/v3/secretsController.ts b/backend/src/controllers/v3/secretsController.ts index cd00f0429..4a57beba3 100644 --- a/backend/src/controllers/v3/secretsController.ts +++ b/backend/src/controllers/v3/secretsController.ts @@ -6,10 +6,9 @@ import { BotService } from "../../services"; import { containsGlobPatterns, repackageSecretToRaw } from "../../helpers/secrets"; import { encryptSymmetric128BitHexKeyUTF8 } from "../../utils/crypto"; import { getAllImportedSecrets } from "../../services/SecretImportService"; -import Folder from "../../models/folder"; +import { Folder, IServiceTokenData } from "../../models"; import { getFolderByPath } from "../../services/FolderService"; import { BadRequestError } from "../../utils/errors"; -import { IServiceTokenData } from "../../models"; import { requireWorkspaceAuth } from "../../middleware"; import { ADMIN, MEMBER, PERMISSION_READ_SECRETS } from "../../variables"; @@ -23,6 +22,7 @@ export const getSecretsRaw = async (req: Request, res: Response) => { let workspaceId = req.query.workspaceId as string; let environment = req.query.environment as string; let secretPath = req.query.secretPath as string; + const folderId = req.query.folderId as string | undefined; const includeImports = req.query.include_imports as string; // if the service token has single scope, it will get all secrets for that scope by default @@ -47,6 +47,7 @@ export const getSecretsRaw = async (req: Request, res: Response) => { const secrets = await SecretService.getSecrets({ workspaceId: new Types.ObjectId(workspaceId), environment, + folderId, secretPath, authData: req.authData }); @@ -284,11 +285,13 @@ export const getSecrets = async (req: Request, res: Response) => { const workspaceId = req.query.workspaceId as string; const environment = req.query.environment as string; const secretPath = req.query.secretPath as string; + const folderId = req.query.folderId as string | undefined; const includeImports = req.query.include_imports as string; const secrets = await SecretService.getSecrets({ workspaceId: new Types.ObjectId(workspaceId), environment, + folderId, secretPath, authData: req.authData }); diff --git a/backend/src/data/common_passwords.txt b/backend/src/data/common_passwords.txt deleted file mode 100644 index 01a442b17..000000000 --- a/backend/src/data/common_passwords.txt +++ /dev/null @@ -1,1497 +0,0 @@ -123456 -123456789 -111111 -password -qwerty -abc123 -12345678 -password1 -1234567 -123123 -1234567890 -000000 -12345 -iloveyou -1q2w3e4r5t -1234 -123456a -qwertyuiop -monkey -123321 -dragon -654321 -666666 -123 -myspace1 -a123456 -121212 -1qaz2wsx -123qwe -123abc -tinkle -target123 -gwerty -1g2w3e4r -gwerty123 -zag12wsx -7777777 -qwerty1 -1q2w3e4r -987654321 -222222 -qwe123 -qwerty123 -zxcvbnm -555555 -112233 -fuckyou -asdfghjkl -12345a -123123123 -1q2w3e -qazwsx -computer -aaaaaa -159753 -iloveyou1 -fuckyou1 -princess -789456123 -11111111 -123654 -princess1 -888888 -linkedin -michael -sunshine -football -11111 -777777 -1234qwer -999999 -j38ifUbn -monkey1 -football1 -daniel -azerty -a12345 -123456789a -789456 -asdfgh -love123 -abcd1234 -jordan23 -88888888 -5201314 -12qwaszx -FQRG7CS493 -ashley -asdf -asd123 -superman -jessica -love -samsung -shadow -blink182 -333333 -michael1 -babygirl1 -jesus1 -qwert -k.: -baseball -charlie -0 -hello1 -soccer -killer -131313 -master -1111111 -gfhjkm -0123456789 -987654 -iloveyou2 -angel1 -jordan -147258369 -bitch1 -michelle -q1w2e3r4 -jessica1 -qwer1234 -159357 -soccer1 -liverpool -101010 -zxcvbn -thomas -asdasd -fuckyou2 -justin -nicole -1111111111 -1 -1111 -qazwsxedc -baseball1 -andrew -hello -apple -0987654321 -anthony1 -102030 -money1 -parola -abc -147258 -anthony -111222 -jennifer -number1 -naruto -123456q -696969 -00000000 -joshua -golfer -29rsavoy -myspace -andrea -basketball -qwerty12 -charlie1 -passw0rd -asshole1 -hunter -marina -welcome -010203 -superman1 -password12 -xbox360 -sunshine1 -ashley1 -lovely -babygirl -! -trustno1 -666 -asdf1234 -chocolate -buster -summer -tigger -purple -freedom -loveme -matthew -50cent -password2 -maggie -george -chelsea -12341234 -amanda -hannah -q1w2e3 -friends -shadow1 -william -abcdefg -samantha -12344321 -nicole1 -q1w2e3r4t5y6 -robert -mother -jordan1 -secret -letmein -qweasdzxc -212121 -pokemon -$HEX -internet -batman -love12 -a123456789 -VQsaBLPzLa -qweqwe -hello123 -232323 -butterfly -martin -flower -forever -mustang -1qazxsw2 -iloveu -cjmasterinf -orange -harley -user -brandon1 -london -1234567891 -pepper -chris1 -lol123 -abcdef -whatever -1342 -alexander -loveyou -290966 -wall.e -junior -12413 -qweasd -PE#5GZ29PTZMSE -tudelft -dpbk1234 -DIOSESFIEL -U38fa39 -147852 -cookie -family -jasmine -dragon1 -12345q -nikita -pakistan -123654789 -123789 -amanda1 -joseph -happy1 -ginger -: -matthew1 -snoopy -justin1 -lastfm -3rJs1la7qE -пїЅпїЅпїЅпїЅпїЅпїЅ -antonio -barcelona -matrix -computer1 -hottie1 -sophie -sandra -michelle1 -12345678910 -qqqqqq -arsenal -444444 -brandon -daniel1 -jonathan -killer1 -liverpool1 -mickey -ghbdtn -purple1 -mercedes -patrick -11223344 -diamond -456789 -victoria -asshole -taylor -qwertyu -andrew1 -red123 -lucky1 -eminem -12345qwert -111222tianya -yellow -william1 -bailey -angel -chicken1 -richard -0000 -banana -0000000000 -jasmine1 -benjamin -welcome1 -starwars -hunter1 -cheese -melissa -angela -christian -1234554321 -oliver -chocolate1 -butterfly1 -peanut -55555 -hockey -mylove -natasha -NULL -mommy1 -1234561 -q1w2e3r4t5 -america -252525 -monster -school -456123 -james1 -slipknot -hannah1 -zaq12wsx -chicken -147852369 -gabriel -elizabeth -cookie1 -Status -87654321 -robert1 -ferrari -nathan -1password -buddy1 -1314520 -america1 -metallica -chelsea1 -zzzzzz -prince -adidas -jackson -morgan -rainbow -silver -1234567a -angels -iw14Fi9j -loveme1 -juventus -jennifer1 -!~!1 -bubbles -samuel -fuckoff -lovers -cheese1 -0123456 -123asd -999999999 -madison -elizabeth1 -music -buster1 -lauren -david1 -tigger1 -123qweasd -taylor1 -carlos -tinkerbell -samantha1 -Sojdlg123aljg -joshua1 -poop -stella -myspace123 -asdasd5 -freedom1 -whatever1 -xxxxxx -00000 -valentina -a1b2c3 -741852963 -austin -monica -qaz123 -lovely1 -music1 -harley1 -family1 -spongebob1 -steven -nirvana -1234abcd -hellokitty -thomas1 -7654321 -madison1 -daddy1 -summer1 -cocacola -nicholas -zxc123 -123456m -qwertyui -spiderman -vanessa -diamond1 -142536 -danielle -badoo -7758521 -bandit -pokemon1 -mustang1 -1qaz2wsx3edc -alexis -loulou -justinbieb -yamaha -qwert1 -scooter -rachel -tennis -ronaldo -i -mexico1 -friends1 -victor -maggie1 -asdfasdf -qwerty12345 -lover1 -jesus -123hfjdk147 -nicolas -batman1 -weed420 -password123 -loser1 -123456j -iloveyou! -pepper1 -fuckoff1 -555666 -iloveu2 -sabrina -pussy1 -bubbles1 -098765 -master1 -smokey -a1b2c3d4 -123456789q -qwaszx -heather -jasper -booboo -heather1 -4815162342 -peanut1 -chester -123456s -123456b -google -edward -yankees1 -canada -Exigent -destiny -success -nigger1 -135790 -asdfghjkl1 -124578 -casper -lalala -mother1 -sexy123 -qazxsw -naruto1 -1q2w3e4r5t6y -david -money -yellow1 -patrick1 -flower1 -12121212 -alexander1 -raiders1 -Password1 -sebastian -134679 -zxcvbnm1 -dennis -852456 -hahaha -daniela -ginger1 -olivia -melissa1 -010101 -slipknot1 -spiderman1 -cowboys1 -0000000 -rebecca -741852 -jeremy -a1234567 -dakota -123456d -1a2b3c -apple1 -november -alexandra -159951 -iloveu1 -veronica -fuckme1 -baby123 -yankees -stupid1 -cristina -newyork1 -jackson1 -playboy -friend -iloveyou12 -sammy1 -pimpin1 -phoenix -PolniyPizdec0211 -rocky1 -password! -joseph1 -753951 -p -a838hfiD -richard1 -beautiful1 -mickey1 -carolina -j123456 -202020 -newyork -patricia -charles -stephanie -orange1 -m123456 -421uiopy258 -myspace2 -cameron -spider -barbie -woaini -vincent -mexico -scorpion -monster1 -aaaaa -elephant -asdf123 -963852741 -zk.: -guitar -fucker1 -destiny1 -hotmail -johnny -doudou -q123456 -bailey1 -asdfgh1 -fucker -louise -sparky -sweety -123456abc -shorty1 -booboo1 -december -9876543210 -manchester -midnight -246810 -jessie -dallas -austin1 -s123456 -pass -12345678a -claudia -пїЅпїЅпїЅпїЅпїЅпїЅпїЅ -kristina -lakers -lovelove -crazy1 -tiger1 -thunder -dolphin -a -gangsta1 -jackie -151515 -charlotte -scooter1 -caroline -fuck -merlin -junior1 -super123 -scooby -marseille -aaaa -metallica1 -kitty1 -chris -beautiful -black1 -danielle1 -blessed1 -skater1 -1029384756 -qazwsx123 -456456 -b123456 -genius -guitar1 -tyler1 -peaches -california -sakura -tigers -soleil -lauren1 -green1 -smokey1 -cooper -520520 -muffin -christian1 -love13 -fucku2 -arsenal1 -lucky7 -diablo -apples -george1 -babyboy1 -crystal -1122334455 -player1 -aa123456 -vfhbyf -forever1 -Password -winston -chivas1 -sexy -hockey1 -1a2b3c4d -pussy -playboy1 -stalker -cherry -tweety -toyota -creative -gemini -pretty1 -пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ -maverick -brittany1 -nathan1 -letmein1 -cameron1 -secret1 -google1 -heaven -martina -murphy -spongebob -uQA9Ebw445 -fernando -pretty -startfinding -softball -dolphin1 -fuckme -test123 -qwerty1234 -kobe24 -alejandro -adrian -september -aaaaaa1 -bubba1 -isabella -abc123456 -password3 -jason1 -abcdefg123 -loveyou1 -shannon -100200 -manuel -leonardo -molly1 -flowers -123456z -007007 -password. -321321 -miguel -samsung1 -sergey -sweet1 -abc1234 -windows -qwert123 -vfrcbv -poohbear -d123456 -school1 -badboy -951753 -123456c -111 -steven1 -snoopy1 -garfield -YAgjecc826 -compaq -candy1 -sarah1 -qwerty123456 -123456l -eminem1 -141414 -789789 -maria -steelers -iloveme1 -morgan1 -winner -boomer -lolita -nastya -alexis1 -carmen -angelo -nicholas1 -portugal -precious -jackass1 -jonathan1 -yfnfif -bitch -tiffany -rabbit -rainbow1 -angel123 -popcorn -barbara -brandy -fuckyou! -starwars1 -barney -natalia -hiphop -tiffany1 -shorty -poohbear1 -simone -albert -marlboro -hardcore -cowboys -sydney -alex -scorpio -1234512345 -q12345 -qq123456 -onelove -bond007 -abcdefg1 -eagles -crystal1 -azertyuiop -winter -sexy12 -angelina -james -svetlana -fatima -123456k -icecream -popcorn1 -121314 -john316 -qazwsx1 -victoria1 -twilight -iloveme -9379992 -pass123 -dancer -brittany -beauty -bonjour -maxwell -coffee -dexter -454545 -qazqaz -snickers -love11 -samson -aaaaaaaa -swordfish -fyfcnfcbz -abcd123 -aaa111 -natalie -hottie -passion -alyssa -rockstar1 -lovers1 -florida -alicia -happy -blue123 -123456t -ranger -yourmom1 -pumpkin -denise -edward1 -tweety1 -christine -august -54321 -bella1 -marie1 -seven7 -steelers1 -aaaaa1 -shannon1 -amber1 -cutie1 -peaches1 -florida1 -bonnie -stephanie1 -lollipop -cassie -k. -rachel1 -greenday1 -krishna -teresa -october -iverson3 -motorola -rockstar -hahaha1 -police -lakers24 -fylhtq -andrey -loveme2 -turtle -southside1 -baby -bismillah -pa55word -blessed -emmanuel -666999 -012345 -fluffy -5555555555 -stupid -karina -fishing -musica -password11 -love4ever -melanie -greenday -isabelle -nothing -abcd -chicago -cowboy -mnbvcxz -andrea1 -242424 -babygurl1 -santiago -ssssss -kevin1 -lakers1 -chester1 -321654 -kimberly -carlos1 -z123456 -daisy1 -jackass -m -5555555 -zoosk -boston -happy123 -55555555 -satan666 -111111a -pamela -090909 -francesco -horses -456852 -qwer -vanessa1 -redsox -pookie -a12345678 -110110 -tucker -marley -corvette -778899 -realmadrid -raiders -rangers -people -1123581321 -soccer12 -sayang -shelby -christ -12345t -fktrcfylh -kitten -player -c123456 -qwert12345 -baby12 -trinity -1v7Upjw3nT -p@ssw0rd -thunder1 -zxcvbnm123 -midnight1 -lebron23 -golden -strawberry -orlando -love1234 -lucky13 -asdfg1 -marine -soccer10123456 -password -12345678 -1234 -pussy -12345 -dragon -qwerty -696969 -mustang -letmein -baseball -master -michael -football -shadow -monkey -abc123 -pass -fuckme -6969 -jordan -harley -ranger -iwantu -jennifer -hunter -fuck -2000 -test -batman -trustno1 -thomas -tigger -robert -access -love -buster -1234567 -soccer -hockey -killer -george -sexy -andrew -charlie -superman -asshole -fuckyou -dallas -jessica -panties -pepper -1111 -austin -william -daniel -golfer -summer -heather -hammer -yankees -joshua -maggie -biteme -enter -ashley -thunder -cowboy -silver -richard -fucker -orange -merlin -michelle -corvette -bigdog -cheese -matthew -121212 -patrick -martin -freedom -ginger -blowjob -nicole -sparky -yellow -camaro -secret -dick -falcon -taylor -111111 -131313 -123123 -bitch -hello -scooter -please -porsche -guitar -chelsea -black -diamond -nascar -jackson -cameron -654321 -computer -amanda -wizard -xxxxxxxx -money -phoenix -mickey -bailey -knight -iceman -tigers -purple -andrea -horny -dakota -aaaaaa -player -sunshine -morgan -starwars -boomer -cowboys -edward -charles -girls -booboo -coffee -xxxxxx -bulldog -ncc1701 -rabbit -peanut -john -johnny -gandalf -spanky -winter -brandy -compaq -carlos -tennis -james -mike -brandon -fender -anthony -blowme -ferrari -cookie -chicken -maverick -chicago -joseph -diablo -sexsex -hardcore -666666 -willie -welcome -chris -panther -yamaha -justin -banana -driver -marine -angels -fishing -david -maddog -hooters -wilson -butthead -dennis -fucking -captain -bigdick -chester -smokey -xavier -steven -viking -snoopy -blue -eagles -winner -samantha -house -miller -flower -jack -firebird -butter -united -turtle -steelers -tiffany -zxcvbn -tomcat -golf -bond007 -bear -tiger -doctor -gateway -gators -angel -junior -thx1138 -porno -badboy -debbie -spider -melissa -booger -1212 -flyers -fish -porn -matrix -teens -scooby -jason -walter -cumshot -boston -braves -yankee -lover -barney -victor -tucker -princess -mercedes -5150 -doggie -zzzzzz -gunner -horney -bubba -2112 -fred -johnson -xxxxx -tits -member -boobs -donald -bigdaddy -bronco -penis -voyager -rangers -birdie -trouble -white -topgun -bigtits -bitches -green -super -qazwsx -magic -lakers -rachel -slayer -scott -2222 -asdf -video -london -7777 -marlboro -srinivas -internet -action -carter -jasper -monster -teresa -jeremy -11111111 -bill -crystal -peter -pussies -cock -beer -rocket -theman -oliver -prince -beach -amateur -7777777 -muffin -redsox -star -testing -shannon -murphy -frank -hannah -dave -eagle1 -11111 -mother -nathan -raiders -steve -forever -angela -viper -ou812 -jake -lovers -suckit -gregory -buddy -whatever -young -nicholas -lucky -helpme -jackie -monica -midnight -college -baby -cunt -brian -mark -startrek -sierra -leather -232323 -4444 -beavis -bigcock -happy -sophie -ladies -naughty -giants -booty -blonde -fucked -golden -0 -fire -sandra -pookie -packers -einstein -dolphins -chevy -winston -warrior -sammy -slut -8675309 -zxcvbnm -nipples -power -victoria -asdfgh -vagina -toyota -travis -hotdog -paris -rock -xxxx -extreme -redskins -erotic -dirty -ford -freddy -arsenal -access14 -wolf -nipple -iloveyou -alex -florida -eric -legend -movie -success -rosebud -jaguar -great -cool -cooper -1313 -scorpio -mountain -madison -987654 -brazil -lauren -japan -naked -squirt -stars -apple -alexis -aaaa -bonnie -peaches -jasmine -kevin -matt -qwertyui -danielle -beaver -4321 -4128 -runner -swimming -dolphin -gordon -casper -stupid -shit -saturn -gemini -apples -august -3333 -canada -blazer -cumming -hunting -kitty -rainbow -112233 -arthur -cream -calvin -shaved -surfer -samson -kelly -paul -mine -king -racing -5555 -eagle -hentai -newyork -little -redwings -smith -sticky -cocacola -animal -broncos -private -skippy -marvin -blondes -enjoy -girl -apollo -parker -qwert -time -sydney -women -voodoo -magnum -juice -abgrtyu -777777 -dreams -maxwell -music -rush2112 -russia -scorpion -rebecca -tester -mistress -phantom -billy -6666 -albert \ No newline at end of file diff --git a/backend/src/ee/controllers/v1/workspaceController.ts b/backend/src/ee/controllers/v1/workspaceController.ts index 9354c4f13..1e21dd591 100644 --- a/backend/src/ee/controllers/v1/workspaceController.ts +++ b/backend/src/ee/controllers/v1/workspaceController.ts @@ -1,6 +1,6 @@ import { Request, Response } from "express"; import { PipelineStage, Types } from "mongoose"; -import { Membership, Secret, ServiceTokenData, User } from "../../../models"; +import { Folder, Membership, Secret, ServiceTokenData, TFolderSchema, User } from "../../../models"; import { ActorType, AuditLog, @@ -18,7 +18,7 @@ import { } from "../../models"; import { EESecretService } from "../../services"; import { getLatestSecretVersionIds } from "../../helpers/secretVersion"; -import Folder, { TFolderSchema } from "../../../models/folder"; +// import Folder, { TFolderSchema } from "../../../models/folder"; import { searchByFolderId } from "../../../services/FolderService"; import { EEAuditLogService, EELicenseService } from "../../services"; import { extractIPDetails, isValidIpOrCidr } from "../../../utils/ip"; diff --git a/backend/src/ee/models/secretVersion.ts b/backend/src/ee/models/secretVersion.ts index d63f05cf7..84174ac87 100644 --- a/backend/src/ee/models/secretVersion.ts +++ b/backend/src/ee/models/secretVersion.ts @@ -117,7 +117,7 @@ const secretVersionSchema = new Schema( ref: "Tag", type: [Schema.Types.ObjectId], default: [], - }, + } }, { timestamps: true, diff --git a/backend/src/helpers/bot.ts b/backend/src/helpers/bot.ts index cf6d31e4f..245fe1192 100644 --- a/backend/src/helpers/bot.ts +++ b/backend/src/helpers/bot.ts @@ -14,7 +14,7 @@ import { } from "../variables"; import { client, getEncryptionKey, getRootEncryptionKey } from "../config"; import { InternalServerError } from "../utils/errors"; -import Folder from "../models/folder"; +import { Folder } from "../models"; import { getFolderByPath } from "../services/FolderService"; import { getAllImportedSecrets } from "../services/SecretImportService"; import { expandSecrets } from "./secrets"; diff --git a/backend/src/helpers/integration.ts b/backend/src/helpers/integration.ts index 5e2887aa6..4b1604d50 100644 --- a/backend/src/helpers/integration.ts +++ b/backend/src/helpers/integration.ts @@ -9,7 +9,6 @@ import { INTEGRATION_VERCEL } from "../variables"; import { UnauthorizedRequestError } from "../utils/errors"; -import { syncSecretsToActiveIntegrationsQueue } from "../queues/integrations/syncSecretsToThirdPartyServices" interface Update { workspace: string; diff --git a/backend/src/helpers/secrets.ts b/backend/src/helpers/secrets.ts index d4544d32b..ad6296adf 100644 --- a/backend/src/helpers/secrets.ts +++ b/backend/src/helpers/secrets.ts @@ -7,11 +7,13 @@ import { UpdateSecretParams } from "../interfaces/services/SecretService"; import { + Folder, ISecret, IServiceTokenData, Secret, SecretBlindIndexData, - ServiceTokenData + ServiceTokenData, + TFolderRootSchema } from "../models"; import { EventType, SecretVersion } from "../ee/models"; import { @@ -29,6 +31,7 @@ import { ALGORITHM_AES_256_GCM, ENCODING_SCHEME_BASE64, ENCODING_SCHEME_UTF8, + K8_USER_AGENT_NAME, SECRET_PERSONAL, SECRET_SHARED } from "../variables"; @@ -45,7 +48,6 @@ import { getAuthDataPayloadIdObj, getAuthDataPayloadUserObj } from "../utils/aut import { getFolderByPath, getFolderIdFromServiceToken } from "../services/FolderService"; import picomatch from "picomatch"; import path from "path"; -import Folder, { TFolderRootSchema } from "../models/folder"; export const isValidScope = ( authPayload: IServiceTokenData, @@ -393,7 +395,8 @@ export const createSecretHelper = async ({ secretCommentTag, folder: folderId, algorithm: ALGORITHM_AES_256_GCM, - keyEncoding: ENCODING_SCHEME_UTF8 + keyEncoding: ENCODING_SCHEME_UTF8, + metadata }).save(); const secretVersion = new SecretVersion({ @@ -496,6 +499,7 @@ export const getSecretsHelper = async ({ workspaceId, environment, authData, + folderId, secretPath = "/" }: GetSecretsParams) => { let secrets: ISecret[] = []; @@ -505,7 +509,10 @@ export const getSecretsHelper = async ({ throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); } } - const folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath); + + if (!folderId) { + folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath); + } // get personal secrets first secrets = await Secret.find({ @@ -567,21 +574,36 @@ export const getSecretsHelper = async ({ const postHogClient = await TelemetryService.getPostHogClient(); - if (postHogClient) { - postHogClient.capture({ - event: "secrets pulled", - distinctId: await TelemetryService.getDistinctId({ - authData - }), - properties: { - numberOfSecrets: secrets.length, - environment, - workspaceId, - folderId, - channel: authData.userAgentType, - userAgent: authData.userAgent - } - }); + // reduce the number of events captured + let shouldRecordK8Event = false + if (authData.userAgent == K8_USER_AGENT_NAME) { + const randomNumber = Math.random(); + if (randomNumber > 0.9) { + shouldRecordK8Event = true + } + } + + const numberOfSignupSecrets = (secrets.filter((secret) => secret?.metadata?.source === "signup")).length; + const atLeastOneNonSignUpSecret = (secrets.length - numberOfSignupSecrets > 0) + + if (postHogClient && atLeastOneNonSignUpSecret) { + const shouldCapture = authData.userAgent !== K8_USER_AGENT_NAME || shouldRecordK8Event; + const approximateForNoneCapturedEvents = secrets.length * 10 + + if (shouldCapture) { + postHogClient.capture({ + event: "secrets pulled", + distinctId: await TelemetryService.getDistinctId({ authData }), + properties: { + numberOfSecrets: shouldRecordK8Event ? approximateForNoneCapturedEvents : secrets.length, + environment, + workspaceId, + folderId, + channel: authData.userAgentType, + userAgent: authData.userAgent + } + }); + } } return secrets; @@ -680,7 +702,7 @@ export const getSecretHelper = async ({ if (postHogClient) { postHogClient.capture({ - event: "secrets pull", + event: "secrets pulled", distinctId: await TelemetryService.getDistinctId({ authData }), diff --git a/backend/src/index.ts b/backend/src/index.ts index 17c030fed..098da2aad 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -24,7 +24,7 @@ import { secretSnapshot as eeSecretSnapshotRouter, users as eeUsersRouter, workspace as eeWorkspaceRouter, - secretScanning as v1SecretScanningRouter, + secretScanning as v1SecretScanningRouter } from "./ee/routes/v1"; import { auth as v1AuthRouter, @@ -58,7 +58,7 @@ import { signup as v2SignupRouter, tags as v2TagsRouter, users as v2UsersRouter, - workspace as v2WorkspaceRouter, + workspace as v2WorkspaceRouter } from "./routes/v2"; import { auth as v3AuthRouter, @@ -70,14 +70,21 @@ import { healthCheck } from "./routes/status"; import { getLogger } from "./utils/logger"; import { RouteNotFoundError } from "./utils/errors"; import { requestErrorHandler } from "./middleware/requestErrorHandler"; -import { getNodeEnv, getPort, getSecretScanningGitAppId, getSecretScanningPrivateKey, getSecretScanningWebhookProxy, getSecretScanningWebhookSecret, getSiteURL } from "./config"; +import { + getNodeEnv, + getPort, + getSecretScanningGitAppId, + getSecretScanningPrivateKey, + getSecretScanningWebhookProxy, + getSecretScanningWebhookSecret, + getSiteURL +} from "./config"; import { setup } from "./utils/setup"; import { syncSecretsToThirdPartyServices } from "./queues/integrations/syncSecretsToThirdPartyServices"; import { githubPushEventSecretScan } from "./queues/secret-scanning/githubScanPushEvent"; -const SmeeClient = require('smee-client') // eslint-disable-line +const SmeeClient = require("smee-client"); // eslint-disable-line const main = async () => { - await setup(); await EELicenseService.initGlobalFeatureSet(); @@ -94,11 +101,15 @@ const main = async () => { }) ); - if (await getSecretScanningGitAppId() && await getSecretScanningWebhookSecret() && await getSecretScanningPrivateKey()) { + if ( + (await getSecretScanningGitAppId()) && + (await getSecretScanningWebhookSecret()) && + (await getSecretScanningPrivateKey()) + ) { const probot = new Probot({ appId: await getSecretScanningGitAppId(), privateKey: await getSecretScanningPrivateKey(), - secret: await getSecretScanningWebhookSecret(), + secret: await getSecretScanningWebhookSecret() }); if ((await getNodeEnv()) != "production") { @@ -106,12 +117,14 @@ const main = async () => { source: await getSecretScanningWebhookProxy(), target: "http://backend:4000/ss-webhook", logger: console - }) + }); - smee.start() + smee.start(); } - app.use(createNodeMiddleware(GithubSecretScanningService, { probot, webhooksPath: "/ss-webhook" })); // secret scanning webhook + app.use( + createNodeMiddleware(GithubSecretScanningService, { probot, webhooksPath: "/ss-webhook" }) + ); // secret scanning webhook } if ((await getNodeEnv()) === "production") { @@ -207,8 +220,8 @@ const main = async () => { server.on("close", async () => { await DatabaseService.closeDatabase(); - syncSecretsToThirdPartyServices.close() - githubPushEventSecretScan.close() + syncSecretsToThirdPartyServices.close(); + githubPushEventSecretScan.close(); }); return server; diff --git a/backend/src/integrations/apps.ts b/backend/src/integrations/apps.ts index 4598ab213..1eef252b1 100644 --- a/backend/src/integrations/apps.ts +++ b/backend/src/integrations/apps.ts @@ -18,6 +18,10 @@ import { INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM, INTEGRATION_FLYIO, INTEGRATION_FLYIO_API_URL, + INTEGRATION_GCP_API_URL, + INTEGRATION_GCP_SECRET_MANAGER, + INTEGRATION_GCP_SECRET_MANAGER_SERVICE_NAME, + INTEGRATION_GCP_SERVICE_USAGE_URL, INTEGRATION_GITHUB, INTEGRATION_GITLAB, INTEGRATION_GITLAB_API_URL, @@ -79,6 +83,11 @@ const getApps = async ({ }) => { let apps: App[] = []; switch (integrationAuth.integration) { + case INTEGRATION_GCP_SECRET_MANAGER: + apps = await getAppsGCPSecretManager({ + accessToken, + }); + break; case INTEGRATION_AZURE_KEY_VAULT: apps = []; break; @@ -210,6 +219,96 @@ const getApps = async ({ return apps; }; +/** + * Return list of apps for GCP secret manager integration + * @param {Object} obj + * @param {String} obj.accessToken - access token for GCP API + * @returns {Object[]} apps - list of GCP projects + * @returns {String} apps.name - name of GCP project + * @returns {String} apps.appId - id of GCP project + */ +const getAppsGCPSecretManager = async ({ accessToken }: { accessToken: string }) => { + + interface GCPApp { + projectNumber: string; + projectId: string; + lifecycleState: "ACTIVE" | "LIFECYCLE_STATE_UNSPECIFIED" | "DELETE_REQUESTED" | "DELETE_IN_PROGRESS"; + name: string; + createTime: string; + parent: { + type: "organization" | "folder" | "project"; + id: string; + } + } + + interface GCPGetProjectsRes { + projects: GCPApp[]; + nextPageToken?: string; + } + + interface GCPGetServiceRes { + name: string; + parent: string; + state: "ENABLED" | "DISABLED" | "STATE_UNSPECIFIED" + } + + let gcpApps: GCPApp[] = []; + const apps: App[] = []; + + const pageSize = 100; + let pageToken: string | undefined; + let hasMorePages = true; + + while (hasMorePages) { + const params = new URLSearchParams({ + pageSize: String(pageSize), + ...(pageToken ? { pageToken } : {}) + }); + + const res: GCPGetProjectsRes = (await standardRequest.get(`${INTEGRATION_GCP_API_URL}/v1/projects`, { + params, + headers: { + "Authorization": `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + }) + ) + .data; + + gcpApps = gcpApps.concat(res.projects); + + if (!res.nextPageToken) { + hasMorePages = false; + } + + pageToken = res.nextPageToken; + } + + for await (const gcpApp of gcpApps) { + try { + const res: GCPGetServiceRes = (await standardRequest.get( + `${INTEGRATION_GCP_SERVICE_USAGE_URL}/v1/projects/${gcpApp.projectId}/services/${INTEGRATION_GCP_SECRET_MANAGER_SERVICE_NAME}`, { + headers: { + "Authorization": `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + )).data; + + if (res.state === "ENABLED") { + apps.push({ + name: gcpApp.name, + appId: gcpApp.projectId + }); + } + } catch { + continue; + } + } + + return apps; +}; + /** * Return list of apps for Heroku integration * @param {Object} obj @@ -751,7 +850,7 @@ const getAppsTeamCity = async ({ }, }) ).data.project.slice(1); - + const apps = res.map((a: any) => { return { name: a.name, diff --git a/backend/src/integrations/exchange.ts b/backend/src/integrations/exchange.ts index dddbb65c6..3201e6ca1 100644 --- a/backend/src/integrations/exchange.ts +++ b/backend/src/integrations/exchange.ts @@ -4,6 +4,8 @@ import { INTEGRATION_AZURE_TOKEN_URL, INTEGRATION_BITBUCKET, INTEGRATION_BITBUCKET_TOKEN_URL, + INTEGRATION_GCP_SECRET_MANAGER, + INTEGRATION_GCP_TOKEN_URL, INTEGRATION_GITHUB, INTEGRATION_GITHUB_TOKEN_URL, INTEGRATION_GITLAB, @@ -13,17 +15,19 @@ import { INTEGRATION_NETLIFY, INTEGRATION_NETLIFY_TOKEN_URL, INTEGRATION_VERCEL, - INTEGRATION_VERCEL_TOKEN_URL, + INTEGRATION_VERCEL_TOKEN_URL } from "../variables"; import { getClientIdAzure, getClientIdBitBucket, + getClientIdGCPSecretManager, getClientIdGitHub, getClientIdGitLab, getClientIdNetlify, getClientIdVercel, getClientSecretAzure, getClientSecretBitBucket, + getClientSecretGCPSecretManager, getClientSecretGitHub, getClientSecretGitLab, getClientSecretHeroku, @@ -113,6 +117,11 @@ const exchangeCode = async ({ let obj = {} as any; switch (integration) { + case INTEGRATION_GCP_SECRET_MANAGER: + obj = await exchangeCodeGCP({ + code, + }); + break; case INTEGRATION_AZURE_KEY_VAULT: obj = await exchangeCodeAzure({ code, @@ -153,6 +162,40 @@ const exchangeCode = async ({ return obj; }; +/** + * Return [accessToken] for GCP OAuth2 code-token exchange + * @param {Object} obj + * @param {String} obj.code - code for code-token exchange + * @returns {Object} obj2 + * @returns {String} obj2.accessToken - access token for GCP API + * @returns {String} obj2.refreshToken - refresh token for GCP API + * @returns {Date} obj2.accessExpiresAt - date of expiration for access token + */ +const exchangeCodeGCP = async ({ code }: { code: string }) => { + const accessExpiresAt = new Date(); + + const res: ExchangeCodeAzureResponse = ( + await standardRequest.post( + INTEGRATION_GCP_TOKEN_URL, + new URLSearchParams({ + grant_type: "authorization_code", + code: code, + client_id: await getClientIdGCPSecretManager(), + client_secret: await getClientSecretGCPSecretManager(), + redirect_uri: `${await getSiteURL()}/integrations/gcp-secret-manager/oauth2/callback`, + } as any) + ) + ).data; + + accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + res.expires_in); + + return { + accessToken: res.access_token, + refreshToken: res.refresh_token, + accessExpiresAt, + }; +}; + /** * Return [accessToken] for Azure OAuth2 code-token exchange * @param param0 diff --git a/backend/src/integrations/sync.ts b/backend/src/integrations/sync.ts index 099ef2804..2867ad44f 100644 --- a/backend/src/integrations/sync.ts +++ b/backend/src/integrations/sync.ts @@ -26,6 +26,8 @@ import { INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM, INTEGRATION_FLYIO, INTEGRATION_FLYIO_API_URL, + INTEGRATION_GCP_SECRET_MANAGER, + INTEGRATION_GCP_SECRET_MANAGER_URL, INTEGRATION_GITHUB, INTEGRATION_GITLAB, INTEGRATION_GITLAB_API_URL, @@ -92,6 +94,13 @@ const syncSecrets = async ({ accessToken: string; }) => { switch (integration.integration) { + case INTEGRATION_GCP_SECRET_MANAGER: + await syncSecretsGCPSecretManager({ + integration, + secrets, + accessToken + }); + break; case INTEGRATION_AZURE_KEY_VAULT: await syncSecretsAzureKeyVault({ integration, @@ -286,6 +295,165 @@ const syncSecrets = async ({ } }; +/** + * Sync/push [secrets] to GCP secret manager project + * @param {Object} obj + * @param {IIntegration} obj.integration - integration details + * @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values) + * @param {String} obj.accessToken - access token for GCP secret manager + */ +const syncSecretsGCPSecretManager = async ({ + integration, + secrets, + accessToken +}: { + integration: IIntegration; + secrets: Record; + accessToken: string; +}) => { + interface GCPSecret { + name: string; + createTime: string; + } + + interface GCPSMListSecretsRes { + secrets?: GCPSecret[]; + totalSize?: number; + nextPageToken?: string; + } + + let gcpSecrets: GCPSecret[] = []; + + const pageSize = 100; + let pageToken: string | undefined; + let hasMorePages = true; + + while (hasMorePages) { + const params = new URLSearchParams({ + pageSize: String(pageSize), + ...(pageToken ? { pageToken } : {}) + }); + + const res: GCPSMListSecretsRes = (await standardRequest.get( + `${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1beta1/projects/${integration.appId}/secrets`, + { + params, + headers: { + "Authorization": `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + )).data; + + if (res.secrets) { + gcpSecrets = gcpSecrets.concat(res.secrets); + } + + if (!res.nextPageToken) { + hasMorePages = false; + } + + pageToken = res.nextPageToken; + } + + const res: { [key: string]: string; } = {}; + + interface GCPLatestSecretVersionAccess { + name: string; + payload: { + data: string; + } + } + + for await (const gcpSecret of gcpSecrets) { + const arr = gcpSecret.name.split("/"); + const key = arr[arr.length - 1]; + + const secretLatest: GCPLatestSecretVersionAccess = (await standardRequest.get( + `${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1beta1/projects/${integration.appId}/secrets/${key}/versions/latest:access`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + )).data; + + res[key] = Buffer.from(secretLatest.payload.data, "base64").toString("utf-8"); + } + + for await (const key of Object.keys(secrets)) { + if (!(key in res)) { + // case: create secret + await standardRequest.post( + `${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1beta1/projects/${integration.appId}/secrets`, + { + replication: { + automatic: {} + } + }, + { + params: { + secretId: key + }, + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ); + + await standardRequest.post( + `${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1beta1/projects/${integration.appId}/secrets/${key}:addVersion`, + { + payload: { + data: Buffer.from(secrets[key].value).toString("base64") + } + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ); + } + } + + for await (const key of Object.keys(res)) { + if (!(key in secrets)) { + // case: delete secret + await standardRequest.delete( + `${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1beta1/projects/${integration.appId}/secrets/${key}`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ); + } else { + // case: update secret + if (secrets[key].value !== res[key]) { + await standardRequest.post( + `${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1beta1/projects/${integration.appId}/secrets/${key}:addVersion`, + { + payload: { + data: Buffer.from(secrets[key].value).toString("base64") + } + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ); + } + } + } +} + /** * Sync/push [secrets] to Azure Key Vault with vault URI [integration.app] * @param {Object} obj @@ -1838,7 +2006,7 @@ const syncSecretsCheckly = async ({ secrets: Record; accessToken: string; }) => { - // get secrets from travis-ci + const getSecretsRes = ( await standardRequest.get(`${INTEGRATION_CHECKLY_API_URL}/v1/variables`, { headers: { @@ -1860,7 +2028,6 @@ const syncSecretsCheckly = async ({ if (!(key in getSecretsRes)) { // case: secret does not exist in checkly // -> add secret - await standardRequest.post( `${INTEGRATION_CHECKLY_API_URL}/v1/variables`, { @@ -2019,7 +2186,7 @@ const syncSecretsTerraformCloud = async ({ }; /** - * Sync/push [secrets] to TeamCity project + * Sync/push [secrets] to TeamCity project (and optionally build config) * @param {Object} obj * @param {IIntegration} obj.integration - integration details * @param {Object} obj.secrets - secrets to push to integration @@ -2041,57 +2208,124 @@ const syncSecretsTeamCity = async ({ value: string; } - // get secrets from Teamcity - const res = ( - await standardRequest.get( - `${integrationAuth.url}/app/rest/projects/id:${integration.appId}/parameters`, + interface TeamCityBuildConfigParameter { + name: string; + value: string; + inherited: boolean; + } + interface GetTeamCityBuildConfigParametersRes { + href: string; + count: number; + property: TeamCityBuildConfigParameter[]; + } + + if (integration.targetEnvironment && integration.targetEnvironmentId) { + // case: sync to specific build-config in TeamCity project + const res = (await standardRequest.get( + `${integrationAuth.url}/app/rest/buildTypes/${integration.targetEnvironmentId}/parameters`, { headers: { Authorization: `Bearer ${accessToken}`, - Accept: "application/json" - } + Accept: "application/json", + }, } - ) - ).data.property.reduce((obj: any, secret: TeamCitySecret) => { - const secretName = secret.name.replace(/^env\./, ""); - return { - ...obj, - [secretName]: secret.value - }; - }, {}); - - for await (const key of Object.keys(secrets)) { - if (!(key in res) || (key in res && secrets[key] !== res[key])) { - // case: secret does not exist in TeamCity or secret value has changed - // -> create/update secret - await standardRequest.post( - `${integrationAuth.url}/app/rest/projects/id:${integration.appId}/parameters`, + )) + .data + .property + .filter((parameter) => !parameter.inherited) + .reduce((obj: any, secret: TeamCitySecret) => { + const secretName = secret.name.replace(/^env\./, ""); + return { + ...obj, + [secretName]: secret.value + }; + }, {}); + + for await (const key of Object.keys(secrets)) { + if (!(key in res) || (key in res && secrets[key].value !== res[key])) { + // case: secret does not exist in TeamCity or secret value has changed + // -> create/update secret + await standardRequest.post(`${integrationAuth.url}/app/rest/buildTypes/${integration.targetEnvironmentId}/parameters`, { - name: `env.${key}`, - value: secrets[key] + name:`env.${key}`, + value: secrets[key].value }, { headers: { Authorization: `Bearer ${accessToken}`, - Accept: "application/json" - } - } - ); + Accept: "application/json", + }, + }); + } } - } - for await (const key of Object.keys(res)) { - if (!(key in secrets)) { - // delete secret - await standardRequest.delete( - `${integrationAuth.url}/app/rest/projects/id:${integration.appId}/parameters/env.${key}`, + for await (const key of Object.keys(res)) { + if (!(key in secrets)) { + // delete secret + await standardRequest.delete( + `${integrationAuth.url}/app/rest/buildTypes/${integration.targetEnvironmentId}/parameters/env.${key}`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json" + } + } + ); + } + } + } else { + // case: sync to TeamCity project + const res = ( + await standardRequest.get( + `${integrationAuth.url}/app/rest/projects/id:${integration.appId}/parameters`, { headers: { Authorization: `Bearer ${accessToken}`, Accept: "application/json" } } - ); + ) + ).data.property.reduce((obj: any, secret: TeamCitySecret) => { + const secretName = secret.name.replace(/^env\./, ""); + return { + ...obj, + [secretName]: secret.value + }; + }, {}); + + for await (const key of Object.keys(secrets)) { + if (!(key in res) || (key in res && secrets[key] !== res[key])) { + // case: secret does not exist in TeamCity or secret value has changed + // -> create/update secret + await standardRequest.post( + `${integrationAuth.url}/app/rest/projects/id:${integration.appId}/parameters`, + { + name: `env.${key}`, + value: secrets[key].value + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json" + } + } + ); + } + } + + for await (const key of Object.keys(res)) { + if (!(key in secrets)) { + // delete secret + await standardRequest.delete( + `${integrationAuth.url}/app/rest/projects/id:${integration.appId}/parameters/env.${key}`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json" + } + } + ); + } } } }; diff --git a/backend/src/interfaces/services/SecretService/index.ts b/backend/src/interfaces/services/SecretService/index.ts index fb3e4442c..9ea82ce94 100644 --- a/backend/src/interfaces/services/SecretService/index.ts +++ b/backend/src/interfaces/services/SecretService/index.ts @@ -25,6 +25,7 @@ export interface CreateSecretParams { export interface GetSecretsParams { workspaceId: Types.ObjectId; environment: string; + folderId?: string; secretPath: string; authData: AuthData; } diff --git a/backend/src/models/apiKeyData.ts b/backend/src/models/apiKeyData.ts index 622e62be1..0b88c5ddb 100644 --- a/backend/src/models/apiKeyData.ts +++ b/backend/src/models/apiKeyData.ts @@ -36,6 +36,4 @@ const apiKeyDataSchema = new Schema( } ); -const APIKeyData = model("APIKeyData", apiKeyDataSchema); - -export default APIKeyData; +export const APIKeyData = model("APIKeyData", apiKeyDataSchema); \ No newline at end of file diff --git a/backend/src/models/backupPrivateKey.ts b/backend/src/models/backupPrivateKey.ts index 01f0dae21..09df1dda7 100644 --- a/backend/src/models/backupPrivateKey.ts +++ b/backend/src/models/backupPrivateKey.ts @@ -68,9 +68,7 @@ const backupPrivateKeySchema = new Schema( } ); -const BackupPrivateKey = model( +export const BackupPrivateKey = model( "BackupPrivateKey", backupPrivateKeySchema ); - -export default BackupPrivateKey; diff --git a/backend/src/models/bot.ts b/backend/src/models/bot.ts index 96107a231..5a5c83b13 100644 --- a/backend/src/models/bot.ts +++ b/backend/src/models/bot.ts @@ -74,6 +74,4 @@ const botSchema = new Schema( } ); -const Bot = model("Bot", botSchema); - -export default Bot; +export const Bot = model("Bot", botSchema); \ No newline at end of file diff --git a/backend/src/models/botKey.ts b/backend/src/models/botKey.ts index b7be364dd..02a6d6ea9 100644 --- a/backend/src/models/botKey.ts +++ b/backend/src/models/botKey.ts @@ -40,6 +40,4 @@ const botKeySchema = new Schema( } ); -const BotKey = model("BotKey", botKeySchema); - -export default BotKey; +export const BotKey = model("BotKey", botKeySchema); \ No newline at end of file diff --git a/backend/src/models/botOrg.ts b/backend/src/models/botOrg.ts index f7d3cd3ae..177294ef9 100644 --- a/backend/src/models/botOrg.ts +++ b/backend/src/models/botOrg.ts @@ -93,6 +93,4 @@ const botOrgSchema = new Schema( } ); -const BotOrg = model("BotOrg", botOrgSchema); - -export default BotOrg; +export const BotOrg = model("BotOrg", botOrgSchema); \ No newline at end of file diff --git a/backend/src/models/folder.ts b/backend/src/models/folder.ts index 46f532c7d..b3016822d 100644 --- a/backend/src/models/folder.ts +++ b/backend/src/models/folder.ts @@ -51,6 +51,4 @@ const folderRootSchema = new Schema( } ); -const Folder = model("Folder", folderRootSchema); - -export default Folder; +export const Folder = model("Folder", folderRootSchema); \ No newline at end of file diff --git a/backend/src/models/incidentContactOrg.ts b/backend/src/models/incidentContactOrg.ts index 16e5e4f02..905b9263f 100644 --- a/backend/src/models/incidentContactOrg.ts +++ b/backend/src/models/incidentContactOrg.ts @@ -23,9 +23,7 @@ const incidentContactOrgSchema = new Schema( } ); -const IncidentContactOrg = model( +export const IncidentContactOrg = model( "IncidentContactOrg", incidentContactOrgSchema -); - -export default IncidentContactOrg; +); \ No newline at end of file diff --git a/backend/src/models/index.ts b/backend/src/models/index.ts index e588364ee..7a431592b 100644 --- a/backend/src/models/index.ts +++ b/backend/src/models/index.ts @@ -1,89 +1,30 @@ -import BackupPrivateKey, { IBackupPrivateKey } from "./backupPrivateKey"; -import Bot, { IBot } from "./bot"; -import BotOrg, { IBotOrg } from "./botOrg"; -import BotKey, { IBotKey } from "./botKey"; -import IncidentContactOrg, { IIncidentContactOrg } from "./incidentContactOrg"; -import Integration, { IIntegration } from "./integration"; -import IntegrationAuth, { IIntegrationAuth } from "./integrationAuth"; -import Key, { IKey } from "./key"; -import Membership, { IMembership } from "./membership"; -import MembershipOrg, { IMembershipOrg } from "./membershipOrg"; -import Organization, { IOrganization } from "./organization"; -import Secret, { ISecret } from "./secret"; -import Folder, { TFolderRootSchema, TFolderSchema } from "./folder"; -import SecretImport, { ISecretImports } from "./secretImports"; -import SecretBlindIndexData, { ISecretBlindIndexData } from "./secretBlindIndexData"; -import ServiceToken, { IServiceToken } from "./serviceToken"; -import ServiceAccount, { IServiceAccount } from "./serviceAccount"; // new -import ServiceAccountKey, { IServiceAccountKey } from "./serviceAccountKey"; // new -import ServiceAccountOrganizationPermission, { IServiceAccountOrganizationPermission } from "./serviceAccountOrganizationPermission"; // new -import ServiceAccountWorkspacePermission, { IServiceAccountWorkspacePermission } from "./serviceAccountWorkspacePermission"; // new -import TokenData, { ITokenData } from "./tokenData"; -import User, { AuthMethod, IUser } from "./user"; -import UserAction, { IUserAction } from "./userAction"; -import Workspace, { IWorkspace } from "./workspace"; -import ServiceTokenData, { IServiceTokenData } from "./serviceTokenData"; -import APIKeyData, { IAPIKeyData } from "./apiKeyData"; -import LoginSRPDetail, { ILoginSRPDetail } from "./loginSRPDetail"; -import TokenVersion, { ITokenVersion } from "./tokenVersion"; - -export { - AuthMethod, - BackupPrivateKey, - IBackupPrivateKey, - Bot, - IBot, - BotOrg, - IBotOrg, - BotKey, - IBotKey, - IncidentContactOrg, - IIncidentContactOrg, - Integration, - IIntegration, - IntegrationAuth, - IIntegrationAuth, - Key, - IKey, - Membership, - IMembership, - MembershipOrg, - IMembershipOrg, - Organization, - IOrganization, - Secret, - ISecret, - Folder, - TFolderRootSchema, - TFolderSchema, - SecretImport, - ISecretImports, - SecretBlindIndexData, - ISecretBlindIndexData, - ServiceToken, - IServiceToken, - ServiceAccount, - IServiceAccount, - ServiceAccountKey, - IServiceAccountKey, - ServiceAccountOrganizationPermission, - IServiceAccountOrganizationPermission, - ServiceAccountWorkspacePermission, - IServiceAccountWorkspacePermission, - TokenData, - ITokenData, - User, - IUser, - UserAction, - IUserAction, - Workspace, - IWorkspace, - ServiceTokenData, - IServiceTokenData, - APIKeyData, - IAPIKeyData, - LoginSRPDetail, - ILoginSRPDetail, - TokenVersion, - ITokenVersion -}; +export * from "./backupPrivateKey"; +export * from "./bot"; +export * from "./botOrg"; +export * from "./botKey"; +export * from "./incidentContactOrg"; +export * from "./integration/integration"; +export * from "./integrationAuth"; +export * from "./key"; +export * from "./membership"; +export * from "./membershipOrg"; +export * from "./organization"; +export * from "./secret"; +export * from "./tag"; +export * from "./folder"; +export * from "./secretImports"; +export * from "./secretBlindIndexData"; +export * from "./serviceToken"; +export * from "./serviceAccount"; +export * from "./serviceAccountKey"; +export * from "./serviceAccountOrganizationPermission"; +export * from "./serviceAccountWorkspacePermission"; +export * from "./tokenData"; +export * from "./user"; +export * from "./userAction"; +export * from "./workspace"; +export * from "./serviceTokenData"; +export * from "./apiKeyData"; +export * from "./loginSRPDetail"; +export * from "./tokenVersion"; +export * from "./webhooks"; \ No newline at end of file diff --git a/backend/src/models/integration/index.ts b/backend/src/models/integration/index.ts new file mode 100644 index 000000000..2ed44cd28 --- /dev/null +++ b/backend/src/models/integration/index.ts @@ -0,0 +1 @@ +export * from "./integration"; \ No newline at end of file diff --git a/backend/src/models/integration.ts b/backend/src/models/integration/integration.ts similarity index 91% rename from backend/src/models/integration.ts rename to backend/src/models/integration/integration.ts index 579a7dbf2..7a7775bac 100644 --- a/backend/src/models/integration.ts +++ b/backend/src/models/integration/integration.ts @@ -10,6 +10,7 @@ import { INTEGRATION_CODEFRESH, INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM, INTEGRATION_FLYIO, + INTEGRATION_GCP_SECRET_MANAGER, INTEGRATION_GITHUB, INTEGRATION_GITLAB, INTEGRATION_HASHICORP_VAULT, @@ -25,8 +26,9 @@ import { INTEGRATION_TRAVISCI, INTEGRATION_VERCEL, INTEGRATION_WINDMILL -} from "../variables"; +} from "../../variables"; import { Schema, Types, model } from "mongoose"; +import { Metadata } from "./types"; export interface IIntegration { _id: Types.ObjectId; @@ -70,8 +72,10 @@ export interface IIntegration { | "digital-ocean-app-platform" | "cloud-66" | "northflank" - | "windmill"; + | "windmill" + | "gcp-secret-manager"; integrationAuth: Types.ObjectId; + metadata: Metadata; } const integrationSchema = new Schema( @@ -167,7 +171,8 @@ const integrationSchema = new Schema( INTEGRATION_BITBUCKET, INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM, INTEGRATION_CLOUD_66, - INTEGRATION_NORTHFLANK + INTEGRATION_NORTHFLANK, + INTEGRATION_GCP_SECRET_MANAGER ], required: true, }, @@ -180,6 +185,9 @@ const integrationSchema = new Schema( type: String, required: true, default: "/", + }, + metadata: { + type: Schema.Types.Mixed } }, { @@ -187,6 +195,4 @@ const integrationSchema = new Schema( } ); -const Integration = model("Integration", integrationSchema); - -export default Integration; +export const Integration = model("Integration", integrationSchema); \ No newline at end of file diff --git a/backend/src/models/integration/types.ts b/backend/src/models/integration/types.ts new file mode 100644 index 000000000..0415a9556 --- /dev/null +++ b/backend/src/models/integration/types.ts @@ -0,0 +1,3 @@ +export type Metadata = { + secretSuffix?: string; +} \ No newline at end of file diff --git a/backend/src/models/integrationAuth.ts b/backend/src/models/integrationAuth.ts index 66a26898f..5dcd3dfe8 100644 --- a/backend/src/models/integrationAuth.ts +++ b/backend/src/models/integrationAuth.ts @@ -12,6 +12,7 @@ import { INTEGRATION_CODEFRESH, INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM, INTEGRATION_FLYIO, + INTEGRATION_GCP_SECRET_MANAGER, INTEGRATION_GITHUB, INTEGRATION_GITLAB, INTEGRATION_HASHICORP_VAULT, @@ -58,7 +59,8 @@ export interface IIntegrationAuth extends Document { | "terraform-cloud" | "teamcity" | "northflank" - | "windmill"; + | "windmill" + | "gcp-secret-manager"; teamId: string; accountId: string; url: string; @@ -111,7 +113,8 @@ const integrationAuthSchema = new Schema( INTEGRATION_BITBUCKET, INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM, INTEGRATION_CLOUD_66, - INTEGRATION_NORTHFLANK + INTEGRATION_NORTHFLANK, + INTEGRATION_GCP_SECRET_MANAGER ], required: true, }, @@ -190,9 +193,7 @@ const integrationAuthSchema = new Schema( } ); -const IntegrationAuth = model( +export const IntegrationAuth = model( "IntegrationAuth", integrationAuthSchema -); - -export default IntegrationAuth; +); \ No newline at end of file diff --git a/backend/src/models/key.ts b/backend/src/models/key.ts index faa37cd86..fcc6e6f60 100644 --- a/backend/src/models/key.ts +++ b/backend/src/models/key.ts @@ -40,6 +40,4 @@ const keySchema = new Schema( } ); -const Key = model("Key", keySchema); - -export default Key; +export const Key = model("Key", keySchema); \ No newline at end of file diff --git a/backend/src/models/loginSRPDetail.ts b/backend/src/models/loginSRPDetail.ts index 8e9e121c5..26f897270 100644 --- a/backend/src/models/loginSRPDetail.ts +++ b/backend/src/models/loginSRPDetail.ts @@ -24,6 +24,4 @@ const loginSRPDetailSchema = new Schema( } ); -const LoginSRPDetail = model("LoginSRPDetail", loginSRPDetailSchema); - -export default LoginSRPDetail; +export const LoginSRPDetail = model("LoginSRPDetail", loginSRPDetailSchema); \ No newline at end of file diff --git a/backend/src/models/membership.ts b/backend/src/models/membership.ts index 0fca743b4..6c32ff64f 100644 --- a/backend/src/models/membership.ts +++ b/backend/src/models/membership.ts @@ -52,6 +52,4 @@ const membershipSchema = new Schema( } ); -const Membership = model("Membership", membershipSchema); - -export default Membership; +export const Membership = model("Membership", membershipSchema); \ No newline at end of file diff --git a/backend/src/models/membershipOrg.ts b/backend/src/models/membershipOrg.ts index 74a09b805..b45f9cfe8 100644 --- a/backend/src/models/membershipOrg.ts +++ b/backend/src/models/membershipOrg.ts @@ -39,9 +39,7 @@ const membershipOrgSchema = new Schema( } ); -const MembershipOrg = model( +export const MembershipOrg = model( "MembershipOrg", membershipOrgSchema -); - -export default MembershipOrg; +); \ No newline at end of file diff --git a/backend/src/models/organization.ts b/backend/src/models/organization.ts index bafcc05f8..1ae3bcb45 100644 --- a/backend/src/models/organization.ts +++ b/backend/src/models/organization.ts @@ -21,6 +21,4 @@ const organizationSchema = new Schema( } ); -const Organization = model("Organization", organizationSchema); - -export default Organization; +export const Organization = model("Organization", organizationSchema); \ No newline at end of file diff --git a/backend/src/models/secret.ts b/backend/src/models/secret.ts index 34a4d7501..b7d7c266d 100644 --- a/backend/src/models/secret.ts +++ b/backend/src/models/secret.ts @@ -31,6 +31,9 @@ export interface ISecret { keyEncoding: "utf8" | "base64"; tags?: string[]; folder?: string; + metadata?: { + [key: string]: string; + } } const secretSchema = new Schema( @@ -131,6 +134,9 @@ const secretSchema = new Schema( type: String, default: "root", }, + metadata: { + type: Schema.Types.Mixed + } }, { timestamps: true, @@ -139,6 +145,4 @@ const secretSchema = new Schema( secretSchema.index({ tags: 1 }, { background: true }); -const Secret = model("Secret", secretSchema); - -export default Secret; +export const Secret = model("Secret", secretSchema); \ No newline at end of file diff --git a/backend/src/models/secretApprovalRequest.ts b/backend/src/models/secretApprovalRequest.ts index 008274dd9..910ca9135 100644 --- a/backend/src/models/secretApprovalRequest.ts +++ b/backend/src/models/secretApprovalRequest.ts @@ -1,5 +1,5 @@ import mongoose, { Schema, model } from "mongoose"; -import Secret, { ISecret } from "./secret"; +import { ISecret, Secret } from "./secret"; interface ISecretApprovalRequest { secret: mongoose.Types.ObjectId; @@ -78,6 +78,4 @@ const secretApprovalRequestSchema = new Schema( } ); -const SecretApprovalRequest = model("SecretApprovalRequest", secretApprovalRequestSchema); - -export default SecretApprovalRequest; +export const SecretApprovalRequest = model("SecretApprovalRequest", secretApprovalRequestSchema); \ No newline at end of file diff --git a/backend/src/models/secretBlindIndexData.ts b/backend/src/models/secretBlindIndexData.ts index ca277d19e..5d0cd976a 100644 --- a/backend/src/models/secretBlindIndexData.ts +++ b/backend/src/models/secretBlindIndexData.ts @@ -53,6 +53,4 @@ const secretBlindIndexDataSchema = new Schema( } ); -const SecretBlindIndexData = model("SecretBlindIndexData", secretBlindIndexDataSchema); - -export default SecretBlindIndexData; \ No newline at end of file +export const SecretBlindIndexData = model("SecretBlindIndexData", secretBlindIndexDataSchema); \ No newline at end of file diff --git a/backend/src/models/secretImports.ts b/backend/src/models/secretImports.ts index 0041abc29..79046a489 100644 --- a/backend/src/models/secretImports.ts +++ b/backend/src/models/secretImports.ts @@ -48,5 +48,4 @@ const secretImportSchema = new Schema( } ); -const SecretImport = model("SecretImports", secretImportSchema); -export default SecretImport; +export const SecretImport = model("SecretImports", secretImportSchema); \ No newline at end of file diff --git a/backend/src/models/serviceAccount.ts b/backend/src/models/serviceAccount.ts index 090d55c21..75ae157c6 100644 --- a/backend/src/models/serviceAccount.ts +++ b/backend/src/models/serviceAccount.ts @@ -48,6 +48,4 @@ const serviceAccountSchema = new Schema( } ); -const ServiceAccount = model("ServiceAccount", serviceAccountSchema); - -export default ServiceAccount; \ No newline at end of file +export const ServiceAccount = model("ServiceAccount", serviceAccountSchema); \ No newline at end of file diff --git a/backend/src/models/serviceAccountKey.ts b/backend/src/models/serviceAccountKey.ts index d442dcb08..538e39103 100644 --- a/backend/src/models/serviceAccountKey.ts +++ b/backend/src/models/serviceAccountKey.ts @@ -39,6 +39,4 @@ const serviceAccountKeySchema = new Schema( } ); -const ServiceAccountKey = model("ServiceAccountKey", serviceAccountKeySchema); - -export default ServiceAccountKey; +export const ServiceAccountKey = model("ServiceAccountKey", serviceAccountKeySchema); \ No newline at end of file diff --git a/backend/src/models/serviceAccountOrganizationPermission.ts b/backend/src/models/serviceAccountOrganizationPermission.ts index 4519bd832..970ec7461 100644 --- a/backend/src/models/serviceAccountOrganizationPermission.ts +++ b/backend/src/models/serviceAccountOrganizationPermission.ts @@ -18,6 +18,4 @@ const serviceAccountOrganizationPermissionSchema = new Schema("ServiceAccountOrganizationPermission", serviceAccountOrganizationPermissionSchema); - -export default ServiceAccountOrganizationPermission; \ No newline at end of file +export const ServiceAccountOrganizationPermission = model("ServiceAccountOrganizationPermission", serviceAccountOrganizationPermissionSchema); \ No newline at end of file diff --git a/backend/src/models/serviceAccountWorkspacePermission.ts b/backend/src/models/serviceAccountWorkspacePermission.ts index 5814923e5..1d3523766 100644 --- a/backend/src/models/serviceAccountWorkspacePermission.ts +++ b/backend/src/models/serviceAccountWorkspacePermission.ts @@ -39,6 +39,4 @@ const serviceAccountWorkspacePermissionSchema = new Schema("ServiceAccountWorkspacePermission", serviceAccountWorkspacePermissionSchema); - -export default ServiceAccountWorkspacePermission; \ No newline at end of file +export const ServiceAccountWorkspacePermission = model("ServiceAccountWorkspacePermission", serviceAccountWorkspacePermissionSchema); \ No newline at end of file diff --git a/backend/src/models/serviceToken.ts b/backend/src/models/serviceToken.ts index ce5fa3c9b..4734a50e2 100644 --- a/backend/src/models/serviceToken.ts +++ b/backend/src/models/serviceToken.ts @@ -56,6 +56,4 @@ const serviceTokenSchema = new Schema( } ); -const ServiceToken = model("ServiceToken", serviceTokenSchema); - -export default ServiceToken; +export const ServiceToken = model("ServiceToken", serviceTokenSchema); \ No newline at end of file diff --git a/backend/src/models/serviceTokenData.ts b/backend/src/models/serviceTokenData.ts index 804184386..ea7d00eaa 100644 --- a/backend/src/models/serviceTokenData.ts +++ b/backend/src/models/serviceTokenData.ts @@ -89,6 +89,4 @@ const serviceTokenDataSchema = new Schema( } ); -const ServiceTokenData = model("ServiceTokenData", serviceTokenDataSchema); - -export default ServiceTokenData; +export const ServiceTokenData = model("ServiceTokenData", serviceTokenDataSchema); \ No newline at end of file diff --git a/backend/src/models/tag.ts b/backend/src/models/tag.ts index 53bf085d3..a5f0bd307 100644 --- a/backend/src/models/tag.ts +++ b/backend/src/models/tag.ts @@ -3,6 +3,7 @@ import { Schema, Types, model } from "mongoose"; export interface ITag { _id: Types.ObjectId; name: string; + tagColor: string; slug: string; user: Types.ObjectId; workspace: Types.ObjectId; @@ -15,6 +16,11 @@ const tagSchema = new Schema( required: true, trim: true, }, + tagColor: { + type: String, + required: false, + trim: true, + }, slug: { type: String, required: true, @@ -44,6 +50,4 @@ const tagSchema = new Schema( tagSchema.index({ slug: 1, workspace: 1 }, { unique: true }) tagSchema.index({ workspace: 1 }) -const Tag = model("Tag", tagSchema); - -export default Tag; +export const Tag = model("Tag", tagSchema); \ No newline at end of file diff --git a/backend/src/models/token.ts b/backend/src/models/token.ts index ab0a69c9e..62d342b0a 100644 --- a/backend/src/models/token.ts +++ b/backend/src/models/token.ts @@ -27,6 +27,4 @@ const tokenSchema = new Schema({ tokenSchema.index({ email: 1 }); -const Token = model("Token", tokenSchema); - -export default Token; +export const Token = model("Token", tokenSchema); \ No newline at end of file diff --git a/backend/src/models/tokenData.ts b/backend/src/models/tokenData.ts index 615c9019d..2544c05f1 100644 --- a/backend/src/models/tokenData.ts +++ b/backend/src/models/tokenData.ts @@ -50,6 +50,4 @@ const tokenDataSchema = new Schema({ timestamps: true, }); -const TokenData = model("TokenData", tokenDataSchema); - -export default TokenData; +export const TokenData = model("TokenData", tokenDataSchema); \ No newline at end of file diff --git a/backend/src/models/tokenVersion.ts b/backend/src/models/tokenVersion.ts index 1103fc316..b162e019e 100644 --- a/backend/src/models/tokenVersion.ts +++ b/backend/src/models/tokenVersion.ts @@ -42,6 +42,4 @@ const tokenVersionSchema = new Schema( } ); -const TokenVersion = model("TokenVersion", tokenVersionSchema); - -export default TokenVersion; \ No newline at end of file +export const TokenVersion = model("TokenVersion", tokenVersionSchema); \ No newline at end of file diff --git a/backend/src/models/user.ts b/backend/src/models/user.ts index 10139c85e..c85c0936b 100644 --- a/backend/src/models/user.ts +++ b/backend/src/models/user.ts @@ -121,6 +121,4 @@ const userSchema = new Schema( } ); -const User = model("User", userSchema); - -export default User; +export const User = model("User", userSchema); \ No newline at end of file diff --git a/backend/src/models/userAction.ts b/backend/src/models/userAction.ts index 11eda05e8..68fae22be 100644 --- a/backend/src/models/userAction.ts +++ b/backend/src/models/userAction.ts @@ -23,6 +23,4 @@ const userActionSchema = new Schema( } ); -const UserAction = model("UserAction", userActionSchema); - -export default UserAction; +export const UserAction = model("UserAction", userActionSchema); \ No newline at end of file diff --git a/backend/src/models/webhooks.ts b/backend/src/models/webhooks.ts index b4a168878..845c6ada2 100644 --- a/backend/src/models/webhooks.ts +++ b/backend/src/models/webhooks.ts @@ -80,6 +80,4 @@ const WebhookSchema = new Schema( } ); -const Webhook = model("Webhook", WebhookSchema); - -export default Webhook; +export const Webhook = model("Webhook", WebhookSchema); \ No newline at end of file diff --git a/backend/src/models/workspace.ts b/backend/src/models/workspace.ts index b3dd28b00..9d7a19fcc 100644 --- a/backend/src/models/workspace.ts +++ b/backend/src/models/workspace.ts @@ -49,6 +49,4 @@ const workspaceSchema = new Schema({ }, }); -const Workspace = model("Workspace", workspaceSchema); - -export default Workspace; \ No newline at end of file +export const Workspace = model("Workspace", workspaceSchema); \ No newline at end of file diff --git a/backend/src/queues/integrations/syncSecretsToThirdPartyServices.ts b/backend/src/queues/integrations/syncSecretsToThirdPartyServices.ts index 2a12c0e5d..3b5e04a3c 100644 --- a/backend/src/queues/integrations/syncSecretsToThirdPartyServices.ts +++ b/backend/src/queues/integrations/syncSecretsToThirdPartyServices.ts @@ -1,6 +1,5 @@ import Queue, { Job } from "bull"; -import Integration from "../../models/integration"; -import IntegrationAuth from "../../models/integrationAuth"; +import { Integration, IntegrationAuth } from "../../models"; import { BotService } from "../../services"; import { getIntegrationAuthAccessHelper } from "../../helpers"; import { syncSecrets } from "../../integrations/sync" @@ -36,6 +35,14 @@ syncSecretsToThirdPartyServices.process(async (job: Job) => { secretPath: integration.secretPath }); + const suffixedSecrets: any = {}; + if (integration.metadata?.secretSuffix) { + for (const key in secrets) { + const newKey = key + integration.metadata?.secretSuffix; + suffixedSecrets[newKey] = secrets[key]; + } + } + const integrationAuth = await IntegrationAuth.findById(integration.integrationAuth); if (!integrationAuth) throw new Error("Failed to find integration auth"); @@ -49,7 +56,7 @@ syncSecretsToThirdPartyServices.process(async (job: Job) => { await syncSecrets({ integration, integrationAuth, - secrets, + secrets: Object.keys(suffixedSecrets).length !== 0 ? suffixedSecrets : secrets, accessId: access.accessId === undefined ? null : access.accessId, accessToken: access.accessToken }); diff --git a/backend/src/queues/secret-scanning/githubScanPushEvent.ts b/backend/src/queues/secret-scanning/githubScanPushEvent.ts index ec7e9e650..71a7e92d4 100644 --- a/backend/src/queues/secret-scanning/githubScanPushEvent.ts +++ b/backend/src/queues/secret-scanning/githubScanPushEvent.ts @@ -1,16 +1,16 @@ import Queue, { Job } from "bull"; import { ProbotOctokit } from "probot" -import { Commit, Committer, Repository } from "@octokit/webhooks-types"; +import { Commit } from "@octokit/webhooks-types"; import TelemetryService from "../../services/TelemetryService"; import { sendMail } from "../../helpers"; import GitRisks from "../../ee/models/gitRisks"; import { MembershipOrg, User } from "../../models"; -import { OWNER, ADMIN } from "../../variables"; +import { ADMIN, OWNER } from "../../variables"; import { convertKeysToLowercase, scanContentAndGetFindings } from "../../ee/services/GithubSecretScanning/helper"; import { getSecretScanningGitAppId, getSecretScanningPrivateKey } from "../../config"; import { SecretMatch } from "../../ee/services/GithubSecretScanning/types"; -export const githubPushEventSecretScan = new Queue('github-push-event-secret-scanning', 'redis://redis:6379'); +export const githubPushEventSecretScan = new Queue("github-push-event-secret-scanning", "redis://redis:6379"); type TScanPushEventQueueDetails = { organizationId: string, diff --git a/backend/src/routes/v1/auth.ts b/backend/src/routes/v1/auth.ts index f373567f2..d9c0d6120 100644 --- a/backend/src/routes/v1/auth.ts +++ b/backend/src/routes/v1/auth.ts @@ -8,7 +8,8 @@ import { AuthMode } from "../../variables"; router.post("/token", validateRequest, authController.getNewToken); -router.post( // TODO endpoint: deprecate (moved to api/v3/auth/login1) +router.post( + // TODO endpoint: deprecate (moved to api/v3/auth/login1) "/login1", authLimiter, body("email").exists().trim().notEmpty().toLowerCase(), @@ -17,7 +18,8 @@ router.post( // TODO endpoint: deprecate (moved to api/v3/auth/login1) authController.login1 ); -router.post( // TODO endpoint: deprecate (moved to api/v3/auth/login2) +router.post( + // TODO endpoint: deprecate (moved to api/v3/auth/login2) "/login2", authLimiter, body("email").exists().trim().notEmpty().toLowerCase(), @@ -30,7 +32,7 @@ router.post( "/logout", authLimiter, requireAuth({ - acceptedAuthModes: [AuthMode.JWT], + acceptedAuthModes: [AuthMode.JWT] }), authController.logout ); @@ -38,24 +40,19 @@ router.post( router.post( "/checkAuth", requireAuth({ - acceptedAuthModes: [AuthMode.JWT], + acceptedAuthModes: [AuthMode.JWT] }), authController.checkAuth ); -router.get( - "/common-passwords", - authLimiter, - authController.getCommonPasswords -); - -router.delete( // TODO endpoint: deprecate (moved to DELETE v2/users/me/sessions) +router.delete( + // TODO endpoint: deprecate (moved to DELETE v2/users/me/sessions) "/sessions", authLimiter, requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), + acceptedAuthModes: [AuthMode.JWT] + }), authController.revokeAllSessions ); -export default router; \ No newline at end of file +export default router; diff --git a/backend/src/routes/v1/integration.ts b/backend/src/routes/v1/integration.ts index c3de5c3b0..a5d32349e 100644 --- a/backend/src/routes/v1/integration.ts +++ b/backend/src/routes/v1/integration.ts @@ -37,6 +37,8 @@ router.post( body("owner").trim(), body("path").trim(), body("region").trim(), + body("metadata").optional().isObject().withMessage("Metadata should be an object"), + body("metadata.secretSuffix").optional().isString().withMessage("Suffix should be a string"), validateRequest, integrationController.createIntegration ); @@ -89,4 +91,4 @@ router.post( integrationController.manualSync ); -export default router; +export default router; \ No newline at end of file diff --git a/backend/src/routes/v1/integrationAuth.ts b/backend/src/routes/v1/integrationAuth.ts index b8b7f348d..edc7314cc 100644 --- a/backend/src/routes/v1/integrationAuth.ts +++ b/backend/src/routes/v1/integrationAuth.ts @@ -168,6 +168,20 @@ router.get( integrationAuthController.getIntegrationAuthNorthflankSecretGroups ); +router.get( + "/:integrationAuthId/teamcity/build-configs", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT], + }), + requireIntegrationAuthorizationAuth({ + acceptedRoles: [ADMIN, MEMBER], + }), + param("integrationAuthId").exists().isString(), + query("appId").exists().isString(), + validateRequest, + integrationAuthController.getIntegrationAuthTeamCityBuildConfigs +); + router.delete( "/:integrationAuthId", requireAuth({ diff --git a/backend/src/routes/v2/environment.ts b/backend/src/routes/v2/environment.ts index 133f0d717..a6d5baa13 100644 --- a/backend/src/routes/v2/environment.ts +++ b/backend/src/routes/v2/environment.ts @@ -16,7 +16,7 @@ import { router.post( "/:workspaceId/environments", requireAuth({ - acceptedAuthModes: [AuthMode.JWT], + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], @@ -32,7 +32,7 @@ router.post( router.put( "/:workspaceId/environments", requireAuth({ - acceptedAuthModes: [AuthMode.JWT], + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], @@ -49,7 +49,7 @@ router.put( router.patch( "/:workspaceId/environments", requireAuth({ - acceptedAuthModes: [AuthMode.JWT], + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], @@ -67,7 +67,7 @@ router.patch( router.delete( "/:workspaceId/environments", requireAuth({ - acceptedAuthModes: [AuthMode.JWT], + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN], @@ -82,7 +82,7 @@ router.delete( router.get( "/:workspaceId/environments", requireAuth({ - acceptedAuthModes: [AuthMode.JWT], + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], }), requireWorkspaceAuth({ acceptedRoles: [MEMBER, ADMIN], diff --git a/backend/src/routes/v2/tags.ts b/backend/src/routes/v2/tags.ts index 7ccfd17cd..aca1b6d8c 100644 --- a/backend/src/routes/v2/tags.ts +++ b/backend/src/routes/v2/tags.ts @@ -48,6 +48,7 @@ router.post( }), param("workspaceId").exists().trim(), body("name").exists().trim(), + body("tagColor").exists().trim(), body("slug").exists().trim(), validateRequest, tagController.createWorkspaceTag diff --git a/backend/src/routes/v3/secrets.ts b/backend/src/routes/v3/secrets.ts index 89f4162b2..648fc57f5 100644 --- a/backend/src/routes/v3/secrets.ts +++ b/backend/src/routes/v3/secrets.ts @@ -1,4 +1,4 @@ -import express, { Request, Response } from "express"; +import express from "express"; const router = express.Router(); import { requireAuth, requireWorkspaceAuth, validateRequest } from "../../middleware"; import { body, param, query } from "express-validator"; @@ -17,6 +17,7 @@ router.get( "/raw", query("workspaceId").optional().isString().trim(), query("environment").optional().isString().trim(), + query("folderId").optional().isString().trim(), query("secretPath").default("/").isString().trim(), query("include_imports").optional().isBoolean().default(false), validateRequest, @@ -144,6 +145,7 @@ router.get( "/", query("workspaceId").exists().isString().trim(), query("environment").exists().isString().trim(), + query("folderId").optional().isString().trim(), query("secretPath").default("/").isString().trim(), validateRequest, requireAuth({ diff --git a/backend/src/services/FolderService.ts b/backend/src/services/FolderService.ts index 81f169f4b..cb4ef4286 100644 --- a/backend/src/services/FolderService.ts +++ b/backend/src/services/FolderService.ts @@ -1,6 +1,6 @@ import { nanoid } from "nanoid"; import { Types } from "mongoose"; -import Folder, { TFolderSchema } from "../models/folder"; +import { Folder, TFolderSchema } from "../models"; import path from "path"; type TAppendFolderDTO = { diff --git a/backend/src/services/SecretImportService.ts b/backend/src/services/SecretImportService.ts index 56c600610..d4442b835 100644 --- a/backend/src/services/SecretImportService.ts +++ b/backend/src/services/SecretImportService.ts @@ -1,7 +1,10 @@ import { Types } from "mongoose"; -import Folder from "../models/folder"; -import Secret, { ISecret } from "../models/secret"; -import SecretImport from "../models/secretImports"; +import { + Folder, + ISecret, + Secret, + SecretImport +} from "../models"; import { getFolderByPath } from "./FolderService"; type TSecretImportFid = { environment: string; folderId: string; secretPath: string }; @@ -54,6 +57,14 @@ export const getAllImportedSecrets = async ( type: "shared" } }, + { + $lookup: { + from: "tags", // note this is the name of the collection in the database, not the Mongoose model name + localField: "tags", + foreignField: "_id", + as: "tags" + } + }, { $group: { _id: { diff --git a/backend/src/services/WebhookService.ts b/backend/src/services/WebhookService.ts index c66f44e47..7506e25c1 100644 --- a/backend/src/services/WebhookService.ts +++ b/backend/src/services/WebhookService.ts @@ -3,7 +3,7 @@ import crypto from "crypto"; import { Types } from "mongoose"; import picomatch from "picomatch"; import { client, getRootEncryptionKey } from "../config"; -import Webhook, { IWebhook } from "../models/webhooks"; +import { IWebhook, Webhook } from "../models"; export const triggerWebhookRequest = async ( { url, encryptedSecretKey, iv, tag }: IWebhook, diff --git a/backend/src/utils/setup/backfillData.ts b/backend/src/utils/setup/backfillData.ts index bdc26c0be..9c301722a 100644 --- a/backend/src/utils/setup/backfillData.ts +++ b/backend/src/utils/setup/backfillData.ts @@ -540,20 +540,26 @@ export const backfillIntegration = async () => { }; export const backfillServiceTokenMultiScope = async () => { - await ServiceTokenData.updateMany( - { - scopes: { - $exists: false - } - }, - [ - { - $set: { - scopes: [{ environment: "$environment", secretPath: "$secretPath" }] + const documentsToUpdate = await ServiceTokenData.find({ scopes: { $exists: false } }); + + for (const doc of documentsToUpdate) { + // Cast doc to any to bypass TypeScript's type checks + const anyDoc = doc as any; + + const environment = anyDoc.environment; + const secretPath = anyDoc.secretPath; + + if (environment && secretPath) { + const updatedScopes = [ + { + environment: environment, + secretPath: secretPath } - } - ] - ); + ]; + + await ServiceTokenData.updateOne({ _id: doc._id }, { $set: { scopes: updatedScopes } }); + } + } console.log("Migration: Service token migration v2 complete"); }; @@ -649,24 +655,25 @@ export const backfillUserAuthMethods = async () => { } ); - await User.updateMany( - { - authProvider: { - $exists: true - }, - authMethods: { - $exists: false - } - }, - [ - { - $set: { - authMethods: ["$authProvider"] + + const documentsToUpdate = await User.find({ + authProvider: { $exists: true }, + authMethods: { $exists: false } + }); + + for (const doc of documentsToUpdate) { + // Cast doc to any to bypass TypeScript's type checks + const anyDoc = doc as any; + + const authProvider = anyDoc.authProvider; + const authMethods = [authProvider]; + + await User.updateOne( + { _id: doc._id }, + { + $set: { authMethods: authMethods }, + $unset: { authProvider: 1, authId: 1 } } - }, - { - $unset: ["authProvider", "authId"] - } - ] -); + ); + } } diff --git a/backend/src/variables/authentication.ts b/backend/src/variables/authentication.ts index 38bd54dd3..be2f35c81 100644 --- a/backend/src/variables/authentication.ts +++ b/backend/src/variables/authentication.ts @@ -2,4 +2,6 @@ export enum AuthMode { JWT = "jwt", SERVICE_TOKEN = "serviceToken", API_KEY = "apiKey" -} \ No newline at end of file +} + +export const K8_USER_AGENT_NAME = "k8-operator" \ No newline at end of file diff --git a/backend/src/variables/integration.ts b/backend/src/variables/integration.ts index b08d2a990..ed69cf5fc 100644 --- a/backend/src/variables/integration.ts +++ b/backend/src/variables/integration.ts @@ -1,17 +1,19 @@ import { getClientIdAzure, getClientIdBitBucket, + getClientIdGCPSecretManager, getClientIdGitHub, getClientIdGitLab, getClientIdHeroku, getClientIdNetlify, - getClientSlugVercel, + getClientSlugVercel } from "../config"; // integrations export const INTEGRATION_AZURE_KEY_VAULT = "azure-key-vault"; export const INTEGRATION_AWS_PARAMETER_STORE = "aws-parameter-store"; export const INTEGRATION_AWS_SECRET_MANAGER = "aws-secret-manager"; +export const INTEGRATION_GCP_SECRET_MANAGER = "gcp-secret-manager"; export const INTEGRATION_HEROKU = "heroku"; export const INTEGRATION_VERCEL = "vercel"; export const INTEGRATION_NETLIFY = "netlify"; @@ -36,35 +38,37 @@ export const INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM = "digital-ocean-app-platfor export const INTEGRATION_CLOUD_66 = "cloud-66"; export const INTEGRATION_NORTHFLANK = "northflank"; export const INTEGRATION_SET = new Set([ + INTEGRATION_GCP_SECRET_MANAGER, INTEGRATION_AZURE_KEY_VAULT, - INTEGRATION_HEROKU, - INTEGRATION_VERCEL, - INTEGRATION_NETLIFY, - INTEGRATION_GITHUB, - INTEGRATION_GITLAB, - INTEGRATION_RENDER, - INTEGRATION_FLYIO, - INTEGRATION_CIRCLECI, - INTEGRATION_LARAVELFORGE, - INTEGRATION_TRAVISCI, - INTEGRATION_TEAMCITY, - INTEGRATION_SUPABASE, - INTEGRATION_CHECKLY, - INTEGRATION_TERRAFORM_CLOUD, - INTEGRATION_HASHICORP_VAULT, - INTEGRATION_CLOUDFLARE_PAGES, - INTEGRATION_CODEFRESH, - INTEGRATION_WINDMILL, - INTEGRATION_BITBUCKET, - INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM, - INTEGRATION_CLOUD_66, - INTEGRATION_NORTHFLANK + INTEGRATION_HEROKU, + INTEGRATION_VERCEL, + INTEGRATION_NETLIFY, + INTEGRATION_GITHUB, + INTEGRATION_GITLAB, + INTEGRATION_RENDER, + INTEGRATION_FLYIO, + INTEGRATION_CIRCLECI, + INTEGRATION_LARAVELFORGE, + INTEGRATION_TRAVISCI, + INTEGRATION_TEAMCITY, + INTEGRATION_SUPABASE, + INTEGRATION_CHECKLY, + INTEGRATION_TERRAFORM_CLOUD, + INTEGRATION_HASHICORP_VAULT, + INTEGRATION_CLOUDFLARE_PAGES, + INTEGRATION_CODEFRESH, + INTEGRATION_WINDMILL, + INTEGRATION_BITBUCKET, + INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM, + INTEGRATION_CLOUD_66, + INTEGRATION_NORTHFLANK ]); // integration types export const INTEGRATION_OAUTH2 = "oauth2"; // integration oauth endpoints +export const INTEGRATION_GCP_TOKEN_URL = "https://accounts.google.com/o/oauth2/token"; export const INTEGRATION_AZURE_TOKEN_URL = "https://login.microsoftonline.com/common/oauth2/v2.0/token"; export const INTEGRATION_HEROKU_TOKEN_URL = "https://id.heroku.com/oauth/token"; export const INTEGRATION_VERCEL_TOKEN_URL = @@ -76,6 +80,7 @@ export const INTEGRATION_GITLAB_TOKEN_URL = "https://gitlab.com/oauth/token"; export const INTEGRATION_BITBUCKET_TOKEN_URL = "https://bitbucket.org/site/oauth2/access_token" // integration apps endpoints +export const INTEGRATION_GCP_API_URL = "https://cloudresourcemanager.googleapis.com"; export const INTEGRATION_HEROKU_API_URL = "https://api.heroku.com"; export const INTEGRATION_GITLAB_API_URL = "https://gitlab.com/api"; export const INTEGRATION_VERCEL_API_URL = "https://api.vercel.com"; @@ -97,6 +102,10 @@ export const INTEGRATION_DIGITAL_OCEAN_API_URL = "https://api.digitalocean.com"; export const INTEGRATION_CLOUD_66_API_URL = "https://app.cloud66.com/api"; export const INTEGRATION_NORTHFLANK_API_URL = "https://api.northflank.com"; +export const INTEGRATION_GCP_SECRET_MANAGER_SERVICE_NAME = "secretmanager.googleapis.com" +export const INTEGRATION_GCP_SECRET_MANAGER_URL = `https://${INTEGRATION_GCP_SECRET_MANAGER_SERVICE_NAME}`; +export const INTEGRATION_GCP_SERVICE_USAGE_URL = "https://serviceusage.googleapis.com"; + export const getIntegrationOptions = async () => { const INTEGRATION_OPTIONS = [ { @@ -272,12 +281,12 @@ export const getIntegrationOptions = async () => { docsLink: "", }, { - name: "Google Cloud Platform", - slug: "gcp", + name: "GCP Secret Manager", + slug: "gcp-secret-manager", image: "Google Cloud Platform.png", - isAvailable: false, - type: "", - clientId: "", + isAvailable: true, + type: "oauth", + clientId: await getClientIdGCPSecretManager(), docsLink: "" }, { diff --git a/cloudformation/ec2-deployment/infisical-ec2-deployment.template b/cloudformation/ec2-deployment/infisical-ec2-deployment.template index 2db93b779..b8a5f4062 100644 --- a/cloudformation/ec2-deployment/infisical-ec2-deployment.template +++ b/cloudformation/ec2-deployment/infisical-ec2-deployment.template @@ -22,7 +22,7 @@ Resources: DocumentDBCluster: Type: "AWS::DocDB::DBCluster" Properties: - EngineVersion: 4.0.0 + EngineVersion: 5.0.0 StorageEncrypted: true MasterUsername: !Ref DocumentDBUsername MasterUserPassword: !Ref DocumentDBPassword @@ -38,7 +38,7 @@ Resources: Type: "AWS::DocDB::DBClusterParameterGroup" Properties: Description: "description" - Family: "docdb4.0" + Family: "docdb5.0" Parameters: tls: "disabled" ttl_monitor: "disabled" @@ -97,6 +97,7 @@ Resources: echo "JWT_SERVICE_SECRET=${!JWT_SERVICE_SECRET}" >> .env echo "MONGO_URL=${!DOCUMENT_DB_CONNECTION_URL}" >> .env echo "HTTPS_ENABLED=false" >> .env + echo "REDIS_URL=redis://redis:6379" >> .env docker-compose up -d @@ -174,4 +175,4 @@ Metadata: x: 270 "y": 90 z: 1 - embeds: [] \ No newline at end of file + embeds: [] diff --git a/docs/api-reference/endpoints/environments/create.mdx b/docs/api-reference/endpoints/environments/create.mdx new file mode 100644 index 000000000..2bb14167c --- /dev/null +++ b/docs/api-reference/endpoints/environments/create.mdx @@ -0,0 +1,4 @@ +--- +title: "Create" +openapi: "POST /api/v2/workspace/{workspaceId}/environments" +--- \ No newline at end of file diff --git a/docs/api-reference/endpoints/environments/delete.mdx b/docs/api-reference/endpoints/environments/delete.mdx new file mode 100644 index 000000000..944e42961 --- /dev/null +++ b/docs/api-reference/endpoints/environments/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v2/workspace/{workspaceId}/environments" +--- \ No newline at end of file diff --git a/docs/api-reference/endpoints/environments/list.mdx b/docs/api-reference/endpoints/environments/list.mdx new file mode 100644 index 000000000..4a9d9068e --- /dev/null +++ b/docs/api-reference/endpoints/environments/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v2/workspace/{workspaceId}/environments" +--- \ No newline at end of file diff --git a/docs/api-reference/endpoints/environments/update.mdx b/docs/api-reference/endpoints/environments/update.mdx new file mode 100644 index 000000000..291344d6c --- /dev/null +++ b/docs/api-reference/endpoints/environments/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PUT /api/v2/workspace/{workspaceId}/environments" +--- \ No newline at end of file diff --git a/docs/api-reference/endpoints/secrets/read-one.mdx b/docs/api-reference/endpoints/secrets/list.mdx similarity index 82% rename from docs/api-reference/endpoints/secrets/read-one.mdx rename to docs/api-reference/endpoints/secrets/list.mdx index 6dbbcd726..eaa4597fb 100644 --- a/docs/api-reference/endpoints/secrets/read-one.mdx +++ b/docs/api-reference/endpoints/secrets/list.mdx @@ -1,6 +1,6 @@ --- -title: "Retrieve" -openapi: "GET /api/v3/secrets/{secretName}" +title: "List" +openapi: "GET /api/v3/secrets/" --- diff --git a/docs/api-reference/endpoints/secrets/read.mdx b/docs/api-reference/endpoints/secrets/read.mdx index ab258bdba..6dbbcd726 100644 --- a/docs/api-reference/endpoints/secrets/read.mdx +++ b/docs/api-reference/endpoints/secrets/read.mdx @@ -1,6 +1,6 @@ --- -title: "Retrieve All" -openapi: "GET /api/v3/secrets/" +title: "Retrieve" +openapi: "GET /api/v3/secrets/{secretName}" --- diff --git a/docs/changelog/overview.mdx b/docs/changelog/overview.mdx index 773642c12..db2958b32 100644 --- a/docs/changelog/overview.mdx +++ b/docs/changelog/overview.mdx @@ -9,6 +9,8 @@ The changelog below reflects new product developments and updates on a monthly b - Release Audit Logs V2. - Add support for GitHub SSO. - Enable users to opt in for multiple authentication methods. +- Improved password requirements including check against [Have I Been Pwnd Password API](https://haveibeenpwned.com/Passwords). +- Added native [GCP Secret Manager integration](https://infisical.com/docs/integrations/cloud/gcp-secret-manager) ## July 2023 @@ -16,17 +18,17 @@ The changelog below reflects new product developments and updates on a monthly b - Redesigned the project/organization experience. - Updated the secrets overview page; users are now able to edit secrets directly from it. - Added native [Laravel Forge integration](https://infisical.com/docs/integrations/cloud/laravel-forge). -- Added native [Codefresh integration](https://infisical.com/docs/integrations/cicd/codefresh) -- Added native [Bitbucket integration](https://infisical.com/docs/integrations/cicd/bitbucket) -- Added native [DigitalOcean App Platform integration](https://infisical.com/docs/integrations/cloud/digital-ocean-app-platform) -- Added native [Cloud66 integration](https://infisical.com/docs/integrations/cloud/cloud-66) -- Added native [Terraform Cloud integration](https://infisical.com/docs/integrations/cloud/terraform-cloud) -- Added native [Northflank integration](https://infisical.com/docs/integrations/cloud/northflank) -- Added native [Windmill integration](https://infisical.com/docs/integrations/cloud/windmill) +- Added native [Codefresh integration](https://infisical.com/docs/integrations/cicd/codefresh). +- Added native [Bitbucket integration](https://infisical.com/docs/integrations/cicd/bitbucket). +- Added native [DigitalOcean App Platform integration](https://infisical.com/docs/integrations/cloud/digital-ocean-app-platform). +- Added native [Cloud66 integration](https://infisical.com/docs/integrations/cloud/cloud-66). +- Added native [Terraform Cloud integration](https://infisical.com/docs/integrations/cloud/terraform-cloud). +- Added native [Northflank integration](https://infisical.com/docs/integrations/cloud/northflank). +- Added native [Windmill integration](https://infisical.com/docs/integrations/cloud/windmill). - Added support for Google SSO. - Added support for [Okta](https://infisical.com/docs/documentation/platform/sso/okta), [Azure AD](https://infisical.com/docs/documentation/platform/sso/azure), and JumpCloud [SAML](https://infisical.com/docs/documentation/platform/saml) authentication. -- Released [folders / path-based secret storage](https://infisical.com/docs/documentation/platform/folder) -- Released [webhooks](https://infisical.com/docs/documentation/platform/webhooks) +- Released [folders / path-based secret storage](https://infisical.com/docs/documentation/platform/folder). +- Released [webhooks](https://infisical.com/docs/documentation/platform/webhooks). ## June 2023 @@ -68,7 +70,7 @@ The changelog below reflects new product developments and updates on a monthly b ## Feb 2023 - Upgraded private key encryption/decryption mechanism to use Argon2id and 256-bit protected keys. -- Added preliminary emai-based 2FA capability +- Added preliminary emai-based 2FA capability. - Added suspicious login alerting if user logs in via new device or IP address. - Added documentation for PM2 integration. - Added secret backups support for the CLI; it now fetches and caches secrets locally to be used in the event of future failed fetch. @@ -93,9 +95,9 @@ The changelog below reflects new product developments and updates on a monthly b - Added native GitHub Actions integration. - Added custom environment names. - Added auto-redeployment capability to the Kubernetes operator. -- (Service Token 2.0) Shortened the length of service tokens -- Added a public-facing API -- Added preliminary access control capability for users to be provisioned read/write access to environments +- (Service Token 2.0) Shortened the length of service tokens. +- Added a public-facing API. +- Added preliminary access control capability for users to be provisioned read/write access to environments. - Performed various web UI optimizations. ## Nov 2022 diff --git a/docs/images/integrations/azure-key-vault/integrations-azure-key-vault-config-aad.png b/docs/images/integrations/azure-key-vault/integrations-azure-key-vault-config-aad.png new file mode 100644 index 000000000..58c20c536 Binary files /dev/null and b/docs/images/integrations/azure-key-vault/integrations-azure-key-vault-config-aad.png differ diff --git a/docs/images/integrations/azure-key-vault/integrations-azure-key-vault-config-credentials-1.png b/docs/images/integrations/azure-key-vault/integrations-azure-key-vault-config-credentials-1.png new file mode 100644 index 000000000..264c48341 Binary files /dev/null and b/docs/images/integrations/azure-key-vault/integrations-azure-key-vault-config-credentials-1.png differ diff --git a/docs/images/integrations/azure-key-vault/integrations-azure-key-vault-config-credentials-2.png b/docs/images/integrations/azure-key-vault/integrations-azure-key-vault-config-credentials-2.png new file mode 100644 index 000000000..087e00468 Binary files /dev/null and b/docs/images/integrations/azure-key-vault/integrations-azure-key-vault-config-credentials-2.png differ diff --git a/docs/images/integrations/azure-key-vault/integrations-azure-key-vault-config-credentials-3.png b/docs/images/integrations/azure-key-vault/integrations-azure-key-vault-config-credentials-3.png new file mode 100644 index 000000000..08481bb84 Binary files /dev/null and b/docs/images/integrations/azure-key-vault/integrations-azure-key-vault-config-credentials-3.png differ diff --git a/docs/images/integrations/azure-key-vault/integrations-azure-key-vault-config-new-app-form.png b/docs/images/integrations/azure-key-vault/integrations-azure-key-vault-config-new-app-form.png new file mode 100644 index 000000000..358e0cb41 Binary files /dev/null and b/docs/images/integrations/azure-key-vault/integrations-azure-key-vault-config-new-app-form.png differ diff --git a/docs/images/integrations/azure-key-vault/integrations-azure-key-vault-config-new-app.png b/docs/images/integrations/azure-key-vault/integrations-azure-key-vault-config-new-app.png new file mode 100644 index 000000000..fa9abd08c Binary files /dev/null and b/docs/images/integrations/azure-key-vault/integrations-azure-key-vault-config-new-app.png differ diff --git a/docs/images/integrations-azure-key-vault-create.png b/docs/images/integrations/azure-key-vault/integrations-azure-key-vault-create.png similarity index 100% rename from docs/images/integrations-azure-key-vault-create.png rename to docs/images/integrations/azure-key-vault/integrations-azure-key-vault-create.png diff --git a/docs/images/integrations-azure-key-vault-vault-uri.png b/docs/images/integrations/azure-key-vault/integrations-azure-key-vault-vault-uri.png similarity index 100% rename from docs/images/integrations-azure-key-vault-vault-uri.png rename to docs/images/integrations/azure-key-vault/integrations-azure-key-vault-vault-uri.png diff --git a/docs/images/integrations-azure-key-vault.png b/docs/images/integrations/azure-key-vault/integrations-azure-key-vault.png similarity index 100% rename from docs/images/integrations-azure-key-vault.png rename to docs/images/integrations/azure-key-vault/integrations-azure-key-vault.png diff --git a/docs/images/integrations/gcp-secret-manager/integrations-gcp-secret-manager-auth.png b/docs/images/integrations/gcp-secret-manager/integrations-gcp-secret-manager-auth.png new file mode 100644 index 000000000..647cff42f Binary files /dev/null and b/docs/images/integrations/gcp-secret-manager/integrations-gcp-secret-manager-auth.png differ diff --git a/docs/images/integrations/gcp-secret-manager/integrations-gcp-secret-manager-config-api-services.png b/docs/images/integrations/gcp-secret-manager/integrations-gcp-secret-manager-config-api-services.png new file mode 100644 index 000000000..59bd43fcd Binary files /dev/null and b/docs/images/integrations/gcp-secret-manager/integrations-gcp-secret-manager-config-api-services.png differ diff --git a/docs/images/integrations/gcp-secret-manager/integrations-gcp-secret-manager-config-credentials.png b/docs/images/integrations/gcp-secret-manager/integrations-gcp-secret-manager-config-credentials.png new file mode 100644 index 000000000..af32df88a Binary files /dev/null and b/docs/images/integrations/gcp-secret-manager/integrations-gcp-secret-manager-config-credentials.png differ diff --git a/docs/images/integrations/gcp-secret-manager/integrations-gcp-secret-manager-config-new-app-form.png b/docs/images/integrations/gcp-secret-manager/integrations-gcp-secret-manager-config-new-app-form.png new file mode 100644 index 000000000..2eba7aaf2 Binary files /dev/null and b/docs/images/integrations/gcp-secret-manager/integrations-gcp-secret-manager-config-new-app-form.png differ diff --git a/docs/images/integrations/gcp-secret-manager/integrations-gcp-secret-manager-config-new-app.png b/docs/images/integrations/gcp-secret-manager/integrations-gcp-secret-manager-config-new-app.png new file mode 100644 index 000000000..b950d8070 Binary files /dev/null and b/docs/images/integrations/gcp-secret-manager/integrations-gcp-secret-manager-config-new-app.png differ diff --git a/docs/images/integrations/gcp-secret-manager/integrations-gcp-secret-manager-create.png b/docs/images/integrations/gcp-secret-manager/integrations-gcp-secret-manager-create.png new file mode 100644 index 000000000..9e1719a59 Binary files /dev/null and b/docs/images/integrations/gcp-secret-manager/integrations-gcp-secret-manager-create.png differ diff --git a/docs/images/integrations/gcp-secret-manager/integrations-gcp-secret-manager.png b/docs/images/integrations/gcp-secret-manager/integrations-gcp-secret-manager.png new file mode 100644 index 000000000..81fd62f72 Binary files /dev/null and b/docs/images/integrations/gcp-secret-manager/integrations-gcp-secret-manager.png differ diff --git a/docs/images/integrations-github-auth.png b/docs/images/integrations/github/integrations-github-auth.png similarity index 100% rename from docs/images/integrations-github-auth.png rename to docs/images/integrations/github/integrations-github-auth.png diff --git a/docs/images/integrations/github/integrations-github-config-credentials.png b/docs/images/integrations/github/integrations-github-config-credentials.png new file mode 100644 index 000000000..82b44c03b Binary files /dev/null and b/docs/images/integrations/github/integrations-github-config-credentials.png differ diff --git a/docs/images/integrations/github/integrations-github-config-dev-settings.png b/docs/images/integrations/github/integrations-github-config-dev-settings.png new file mode 100644 index 000000000..e95e94321 Binary files /dev/null and b/docs/images/integrations/github/integrations-github-config-dev-settings.png differ diff --git a/docs/images/integrations/github/integrations-github-config-new-app-form.png b/docs/images/integrations/github/integrations-github-config-new-app-form.png new file mode 100644 index 000000000..8505896e9 Binary files /dev/null and b/docs/images/integrations/github/integrations-github-config-new-app-form.png differ diff --git a/docs/images/integrations/github/integrations-github-config-new-app.png b/docs/images/integrations/github/integrations-github-config-new-app.png new file mode 100644 index 000000000..64f552564 Binary files /dev/null and b/docs/images/integrations/github/integrations-github-config-new-app.png differ diff --git a/docs/images/integrations/github/integrations-github-config-settings.png b/docs/images/integrations/github/integrations-github-config-settings.png new file mode 100644 index 000000000..27547dbfe Binary files /dev/null and b/docs/images/integrations/github/integrations-github-config-settings.png differ diff --git a/docs/images/integrations-github.png b/docs/images/integrations/github/integrations-github.png similarity index 100% rename from docs/images/integrations-github.png rename to docs/images/integrations/github/integrations-github.png diff --git a/docs/images/integrations-gitlab-auth.png b/docs/images/integrations/gitlab/integrations-gitlab-auth.png similarity index 100% rename from docs/images/integrations-gitlab-auth.png rename to docs/images/integrations/gitlab/integrations-gitlab-auth.png diff --git a/docs/images/integrations/gitlab/integrations-gitlab-config-credentials.png b/docs/images/integrations/gitlab/integrations-gitlab-config-credentials.png new file mode 100644 index 000000000..429538a5e Binary files /dev/null and b/docs/images/integrations/gitlab/integrations-gitlab-config-credentials.png differ diff --git a/docs/images/integrations/gitlab/integrations-gitlab-config-edit-profile.png b/docs/images/integrations/gitlab/integrations-gitlab-config-edit-profile.png new file mode 100644 index 000000000..c6eb4d95a Binary files /dev/null and b/docs/images/integrations/gitlab/integrations-gitlab-config-edit-profile.png differ diff --git a/docs/images/integrations/gitlab/integrations-gitlab-config-new-app-form.png b/docs/images/integrations/gitlab/integrations-gitlab-config-new-app-form.png new file mode 100644 index 000000000..b80b5f8d9 Binary files /dev/null and b/docs/images/integrations/gitlab/integrations-gitlab-config-new-app-form.png differ diff --git a/docs/images/integrations/gitlab/integrations-gitlab-config-new-app.png b/docs/images/integrations/gitlab/integrations-gitlab-config-new-app.png new file mode 100644 index 000000000..fac7490a6 Binary files /dev/null and b/docs/images/integrations/gitlab/integrations-gitlab-config-new-app.png differ diff --git a/docs/images/integrations-gitlab-create.png b/docs/images/integrations/gitlab/integrations-gitlab-create.png similarity index 100% rename from docs/images/integrations-gitlab-create.png rename to docs/images/integrations/gitlab/integrations-gitlab-create.png diff --git a/docs/images/integrations-gitlab.png b/docs/images/integrations/gitlab/integrations-gitlab.png similarity index 100% rename from docs/images/integrations-gitlab.png rename to docs/images/integrations/gitlab/integrations-gitlab.png diff --git a/docs/images/integrations-heroku-auth.png b/docs/images/integrations/heroku/integrations-heroku-auth.png similarity index 100% rename from docs/images/integrations-heroku-auth.png rename to docs/images/integrations/heroku/integrations-heroku-auth.png diff --git a/docs/images/integrations/heroku/integrations-heroku-config-applications.png b/docs/images/integrations/heroku/integrations-heroku-config-applications.png new file mode 100644 index 000000000..cca0f078e Binary files /dev/null and b/docs/images/integrations/heroku/integrations-heroku-config-applications.png differ diff --git a/docs/images/integrations/heroku/integrations-heroku-config-credentials.png b/docs/images/integrations/heroku/integrations-heroku-config-credentials.png new file mode 100644 index 000000000..e84aaca01 Binary files /dev/null and b/docs/images/integrations/heroku/integrations-heroku-config-credentials.png differ diff --git a/docs/images/integrations/heroku/integrations-heroku-config-new-app-form.png b/docs/images/integrations/heroku/integrations-heroku-config-new-app-form.png new file mode 100644 index 000000000..dee5a5336 Binary files /dev/null and b/docs/images/integrations/heroku/integrations-heroku-config-new-app-form.png differ diff --git a/docs/images/integrations/heroku/integrations-heroku-config-new-app.png b/docs/images/integrations/heroku/integrations-heroku-config-new-app.png new file mode 100644 index 000000000..c3796c09d Binary files /dev/null and b/docs/images/integrations/heroku/integrations-heroku-config-new-app.png differ diff --git a/docs/images/integrations/heroku/integrations-heroku-config-settings.png b/docs/images/integrations/heroku/integrations-heroku-config-settings.png new file mode 100644 index 000000000..3e40dce6a Binary files /dev/null and b/docs/images/integrations/heroku/integrations-heroku-config-settings.png differ diff --git a/docs/images/integrations-heroku-create.png b/docs/images/integrations/heroku/integrations-heroku-create.png similarity index 100% rename from docs/images/integrations-heroku-create.png rename to docs/images/integrations/heroku/integrations-heroku-create.png diff --git a/docs/images/integrations-heroku.png b/docs/images/integrations/heroku/integrations-heroku.png similarity index 100% rename from docs/images/integrations-heroku.png rename to docs/images/integrations/heroku/integrations-heroku.png diff --git a/docs/images/integrations-netlify-auth.png b/docs/images/integrations/netlify/integrations-netlify-auth.png similarity index 100% rename from docs/images/integrations-netlify-auth.png rename to docs/images/integrations/netlify/integrations-netlify-auth.png diff --git a/docs/images/integrations/netlify/integrations-netlify-config-credentials.png b/docs/images/integrations/netlify/integrations-netlify-config-credentials.png new file mode 100644 index 000000000..6058ccc96 Binary files /dev/null and b/docs/images/integrations/netlify/integrations-netlify-config-credentials.png differ diff --git a/docs/images/integrations/netlify/integrations-netlify-config-new-app-form.png b/docs/images/integrations/netlify/integrations-netlify-config-new-app-form.png new file mode 100644 index 000000000..982d4c621 Binary files /dev/null and b/docs/images/integrations/netlify/integrations-netlify-config-new-app-form.png differ diff --git a/docs/images/integrations/netlify/integrations-netlify-config-new-app.png b/docs/images/integrations/netlify/integrations-netlify-config-new-app.png new file mode 100644 index 000000000..6f45baf25 Binary files /dev/null and b/docs/images/integrations/netlify/integrations-netlify-config-new-app.png differ diff --git a/docs/images/integrations/netlify/integrations-netlify-config-user-settings.png b/docs/images/integrations/netlify/integrations-netlify-config-user-settings.png new file mode 100644 index 000000000..8963cfb2b Binary files /dev/null and b/docs/images/integrations/netlify/integrations-netlify-config-user-settings.png differ diff --git a/docs/images/integrations-netlify-create.png b/docs/images/integrations/netlify/integrations-netlify-create.png similarity index 100% rename from docs/images/integrations-netlify-create.png rename to docs/images/integrations/netlify/integrations-netlify-create.png diff --git a/docs/images/integrations-netlify.png b/docs/images/integrations/netlify/integrations-netlify.png similarity index 100% rename from docs/images/integrations-netlify.png rename to docs/images/integrations/netlify/integrations-netlify.png diff --git a/docs/images/integrations/teamcity/integrations-teamcity-auth.png b/docs/images/integrations/teamcity/integrations-teamcity-auth.png new file mode 100644 index 000000000..6f555a052 Binary files /dev/null and b/docs/images/integrations/teamcity/integrations-teamcity-auth.png differ diff --git a/docs/images/integrations/teamcity/integrations-teamcity-create.png b/docs/images/integrations/teamcity/integrations-teamcity-create.png new file mode 100644 index 000000000..d851863f2 Binary files /dev/null and b/docs/images/integrations/teamcity/integrations-teamcity-create.png differ diff --git a/docs/images/integrations/teamcity/integrations-teamcity-dashboard.png b/docs/images/integrations/teamcity/integrations-teamcity-dashboard.png new file mode 100644 index 000000000..e519be110 Binary files /dev/null and b/docs/images/integrations/teamcity/integrations-teamcity-dashboard.png differ diff --git a/docs/images/integrations/teamcity/integrations-teamcity-token.png b/docs/images/integrations/teamcity/integrations-teamcity-token.png new file mode 100644 index 000000000..caf3896d6 Binary files /dev/null and b/docs/images/integrations/teamcity/integrations-teamcity-token.png differ diff --git a/docs/images/integrations/teamcity/integrations-teamcity.png b/docs/images/integrations/teamcity/integrations-teamcity.png new file mode 100644 index 000000000..5caa2110c Binary files /dev/null and b/docs/images/integrations/teamcity/integrations-teamcity.png differ diff --git a/docs/images/integrations-vercel-auth.png b/docs/images/integrations/vercel/integrations-vercel-auth.png similarity index 100% rename from docs/images/integrations-vercel-auth.png rename to docs/images/integrations/vercel/integrations-vercel-auth.png diff --git a/docs/images/integrations/vercel/integrations-vercel-config-credentials.png b/docs/images/integrations/vercel/integrations-vercel-config-credentials.png new file mode 100644 index 000000000..d00a27eb4 Binary files /dev/null and b/docs/images/integrations/vercel/integrations-vercel-config-credentials.png differ diff --git a/docs/images/integrations/vercel/integrations-vercel-config-integrations-console.png b/docs/images/integrations/vercel/integrations-vercel-config-integrations-console.png new file mode 100644 index 000000000..d6c8cc8b6 Binary files /dev/null and b/docs/images/integrations/vercel/integrations-vercel-config-integrations-console.png differ diff --git a/docs/images/integrations/vercel/integrations-vercel-config-new-app-form-1.png b/docs/images/integrations/vercel/integrations-vercel-config-new-app-form-1.png new file mode 100644 index 000000000..0506a926a Binary files /dev/null and b/docs/images/integrations/vercel/integrations-vercel-config-new-app-form-1.png differ diff --git a/docs/images/integrations/vercel/integrations-vercel-config-new-app-form-2.png b/docs/images/integrations/vercel/integrations-vercel-config-new-app-form-2.png new file mode 100644 index 000000000..8df5ad5ef Binary files /dev/null and b/docs/images/integrations/vercel/integrations-vercel-config-new-app-form-2.png differ diff --git a/docs/images/integrations/vercel/integrations-vercel-config-new-app.png b/docs/images/integrations/vercel/integrations-vercel-config-new-app.png new file mode 100644 index 000000000..d1f174e0a Binary files /dev/null and b/docs/images/integrations/vercel/integrations-vercel-config-new-app.png differ diff --git a/docs/images/integrations-vercel-create.png b/docs/images/integrations/vercel/integrations-vercel-create.png similarity index 100% rename from docs/images/integrations-vercel-create.png rename to docs/images/integrations/vercel/integrations-vercel-create.png diff --git a/docs/images/integrations-vercel.png b/docs/images/integrations/vercel/integrations-vercel.png similarity index 100% rename from docs/images/integrations-vercel.png rename to docs/images/integrations/vercel/integrations-vercel.png diff --git a/docs/integrations/cicd/githubactions.mdx b/docs/integrations/cicd/githubactions.mdx index 5ecf68d03..272594dfa 100644 --- a/docs/integrations/cicd/githubactions.mdx +++ b/docs/integrations/cicd/githubactions.mdx @@ -3,7 +3,9 @@ title: "GitHub Actions" description: "How to sync secrets from Infisical to GitHub Actions" --- - + + + Infisical can sync secrets to GitHub repo secrets only. If your repo uses environment secrets, then stay tuned with this [issue](https://github.com/Infisical/infisical/issues/54). @@ -20,7 +22,7 @@ Prerequisites: Press on the GitHub tile and grant Infisical access to your GitHub account (repo privileges only). -![integrations github authorization](../../images/integrations-github-auth.png) +![integrations github authorization](../../images/integrations/github/integrations-github-auth.png) If this is your project's first cloud integration, then you'll have to grant Infisical access to your project's environment variables. @@ -31,5 +33,43 @@ Press on the GitHub tile and grant Infisical access to your GitHub account (repo Select which Infisical environment secrets you want to sync to which GitHub repo and press start integration to start syncing secrets to the repo. -![integrations github](../../images/integrations-github.png) +![integrations github](../../images/integrations/github/integrations-github.png) + + + Using the GitHub integration on a self-hosted instance of Infisical requires configuring an OAuth application in GitHub + and registering your instance with it. + + ## Create an OAuth application in GitHub + + Navigate to your user Settings > Developer settings > OAuth Apps to create a new GitHub OAuth application. + + ![integrations github config](../../images/integrations/github/integrations-github-config-settings.png) + ![integrations github config](../../images/integrations/github/integrations-github-config-dev-settings.png) + ![integrations github config](../../images/integrations/github/integrations-github-config-new-app.png) + + Create the OAuth application. As part of the form, set the **Homepage URL** to your self-hosted domain `https://your-domain.com` + and the **Authorization callback URL** to `https://your-domain.com/integrations/github/oauth2/callback`. + + ![integrations github config](../../images/integrations/github/integrations-github-config-new-app-form.png) + + + If you have a GitHub organization, you can create an OAuth application under it + in your organization Settings > Developer settings > OAuth Apps > New Org OAuth App. + + + ## Add your OAuth application credentials to Infisical + + Obtain the **Client ID** and generate a new **Client Secret** for your GitHub OAuth application. + + ![integrations github config](../../images/integrations/github/integrations-github-config-credentials.png) + + Back in your Infisical instance, add two new environment variables for the credentials of your GitHub OAuth application: + + - `CLIENT_ID_GITHUB`: The **Client ID** of your GitHub OAuth application. + - `CLIENT_SECRET_GITHUB`: The **Client Secret** of your GitHub OAuth application. + + Once added, restart your Infisical instance and use the GitHub integration. + + + diff --git a/docs/integrations/cicd/gitlab.mdx b/docs/integrations/cicd/gitlab.mdx index 742eee673..d7b2637c6 100644 --- a/docs/integrations/cicd/gitlab.mdx +++ b/docs/integrations/cicd/gitlab.mdx @@ -3,12 +3,14 @@ title: "GitLab" description: "How to sync secrets from Infisical to GitLab" --- -Prerequisites: + + + Prerequisites: - Set up and add envars to [Infisical Cloud](https://app.infisical.com) - - + + ## Navigate to your project's integrations tab ![integrations](../../images/integrations.png) @@ -17,7 +19,7 @@ Prerequisites: Press on the GitLab tile and grant Infisical access to your GitLab account. -![integrations gitlab authorization](../../images/integrations-gitlab-auth.png) +![integrations gitlab authorization](../../images/integrations/gitlab/integrations-gitlab-auth.png) If this is your project's first cloud integration, then you'll have to grant @@ -29,13 +31,11 @@ Press on the GitLab tile and grant Infisical access to your GitLab account. Select which Infisical environment secrets you want to sync to which GitLab repository and press create integration to start syncing secrets to GitLab. -![integrations gitlab](../../images/integrations-gitlab-create.png) -![integrations gitlab](../../images/integrations-gitlab.png) - - - - -## Generate service token +![integrations gitlab](../../images/integrations/gitlab/integrations-gitlab-create.png) +![integrations gitlab](../../images/integrations/gitlab/integrations-gitlab.png) + + + ## Generate service token Generate an [Infisical Token](/documentation/platform/token) for the specific project and environment in Infisical. @@ -65,6 +65,42 @@ build-job: - apt-get update && apt-get install -y infisical - infisical run -- npm run build ``` + + + + + Using the GitLab integration on a self-hosted instance of Infisical requires configuring an application in GitLab + and registering your instance with it. + ## Create an OAuth application in GitLab + + Navigate to your user Settings > Applications to create a new GitLab application. + + ![integrations gitlab config](../../images/integrations/gitlab/integrations-gitlab-config-edit-profile.png) + ![integrations gitlab config](../../images/integrations/gitlab/integrations-gitlab-config-new-app.png) + + Create the application. As part of the form, set the **Redirect URI** to `https://your-domain.com/integrations/gitlab/oauth2/callback`. + + ![integrations gitlab config](../../images/integrations/gitlab/integrations-gitlab-config-new-app-form.png) + + + If you have a GitLab group, you can create an OAuth application under it + in your group Settings > Applications. + + + ## Add your OAuth application credentials to Infisical + + Obtain the **Application ID** and **Secret** for your GitLab application. + + ![integrations gitlab config](../../images/integrations/gitlab/integrations-gitlab-config-credentials.png) + + Back in your Infisical instance, add two new environment variables for the credentials of your GitLab application: + + - `CLIENT_ID_GITLAB`: The **Client ID** of your GitLab application. + - `CLIENT_SECRET_GITLAB`: The **Client Secret** of your GitLab application. + + Once added, restart your Infisical instance and use the GitLab integration. + + diff --git a/docs/integrations/cloud/azure-key-vault.mdx b/docs/integrations/cloud/azure-key-vault.mdx index 90c630666..77e03d555 100644 --- a/docs/integrations/cloud/azure-key-vault.mdx +++ b/docs/integrations/cloud/azure-key-vault.mdx @@ -3,7 +3,9 @@ title: "Azure Key Vault" description: "How to sync secrets from Infisical to Azure Key Vault" --- -Prerequisites: + + + Prerequisites: - Set up and add envars to [Infisical Cloud](https://app.infisical.com) - Set up Azure and have an existing key vault @@ -20,13 +22,13 @@ Press on the Azure Key Vault tile and grant Infisical access to Azure Key Vault. Obtain the Vault URI of your key vault in the Overview tab. -![integrations](../../images/integrations-azure-key-vault-vault-uri.png) +![integrations](../../images/integrations/azure-key-vault/integrations-azure-key-vault-vault-uri.png) Select which Infisical environment secrets you want to sync to your key vault. Then, input your Vault URI from the previous step. Finally, press create integration to start syncing secrets to Azure Key Vault. -![integrations](../../images/integrations-azure-key-vault-create.png) +![integrations](../../images/integrations/azure-key-vault/integrations-azure-key-vault-create.png) -![integrations](../../images/integrations-azure-key-vault.png) +![integrations](../../images/integrations/azure-key-vault/integrations-azure-key-vault.png) If this is your project's first cloud integration, then you'll have to grant @@ -34,3 +36,38 @@ Select which Infisical environment secrets you want to sync to your key vault. T breaks E2EE, it's necessary for Infisical to sync the environment variables to the cloud platform. + + + Using the Azure KV integration on a self-hosted instance of Infisical requires configuring an application in Azure + and registering your instance with it. + + ## Create an application in Azure + + Navigate to Azure Active Directory > App registrations to create a new application. + + ![integrations Azure KV config](../../images/integrations/azure-key-vault/integrations-azure-key-vault-config-aad.png) + ![integrations Azure KV config](../../images/integrations/azure-key-vault/integrations-azure-key-vault-config-new-app.png) + + Create the application. As part of the form, set the **Redirect URI** to `https://your-domain.com/integrations/azure-key-vault/oauth2/callback`. + + ![integrations Azure KV config](../../images/integrations/azure-key-vault/integrations-azure-key-vault-config-new-app-form.png) + + ## Add your application credentials to Infisical + + Obtain the **Application (Client) ID** in Overview and generate a **Client Secret** in Certificate & secrets for your Azure application. + + ![integrations Azure KV config](../../images/integrations/azure-key-vault/integrations-azure-key-vault-config-credentials-1.png) + ![integrations Azure KV config](../../images/integrations/azure-key-vault/integrations-azure-key-vault-config-credentials-2.png) + ![integrations Azure KV config](../../images/integrations/azure-key-vault/integrations-azure-key-vault-config-credentials-3.png) + + Back in your Infisical instance, add two new environment variables for the credentials of your Azure application. + + - `CLIENT_ID_AZURE`: The **Application (Client) ID** of your Azure application. + - `CLIENT_SECRET_AZURE`: The **Client Secret** of your Azure application. + + Once added, restart your Infisical instance and use the Azure KV integration. + + + + + diff --git a/docs/integrations/cloud/checkly.mdx b/docs/integrations/cloud/checkly.mdx index 90c0850d3..315764ce8 100644 --- a/docs/integrations/cloud/checkly.mdx +++ b/docs/integrations/cloud/checkly.mdx @@ -35,3 +35,9 @@ Select which Infisical environment secrets you want to sync to Checkly and press ![integrations checkly](../../images/integrations-checkly-create.png) ![integrations checkly](../../images/integrations-checkly.png) + + + In the new version of the Checkly integration, you are able to specify suffixes that depend on the secrets' environment and path. + If you choose to do so, you should utilize such suffixes for ALL Checkly integrations – otherwise the integration system + might run into issues with deleting secrets from the wrong environments. + diff --git a/docs/integrations/cloud/gcp-secret-manager.mdx b/docs/integrations/cloud/gcp-secret-manager.mdx new file mode 100644 index 000000000..6033f7e64 --- /dev/null +++ b/docs/integrations/cloud/gcp-secret-manager.mdx @@ -0,0 +1,71 @@ +--- +title: "GCP Secret Manager" +description: "How to sync secrets from Infisical to GCP Secret Manager" +--- + + + + Prerequisites: + +- Set up and add envars to [Infisical Cloud](https://app.infisical.com) + +## Navigate to your project's integrations tab + +![integrations](../../images/integrations.png) + +## Authorize Infisical for GCP + +Press on the GCP Secret Manager tile and grant Infisical access to GCP. + +![integrations GCP authorization](../../images/integrations/gcp-secret-manager/integrations-gcp-secret-manager-auth.png) + + + If this is your project's first cloud integration, then you'll have to grant + Infisical access to your project's environment variables. Although this step + breaks E2EE, it's necessary for Infisical to sync the environment variables to + the cloud platform. + + +## Start integration + +Select which Infisical environment secrets you want to sync to which GCP secret manager project. Lastly, press create integration to start syncing secrets to GCP secret manager. + +![integrations GCP secret manager](../../images/integrations/gcp-secret-manager/integrations-gcp-secret-manager-create.png) +![integrations GCP secret manager](../../images/integrations/gcp-secret-manager/integrations-gcp-secret-manager.png) + + + Using Infisical to sync secrets to GCP Secret Manager requires that you enable + the Service Usage API in the Google Cloud project you want to sync secrets to. More on that [here](https://cloud.google.com/service-usage/docs/set-up-development-environment). + + + + Using the GCP Secret Manager integration on a self-hosted instance of Infisical requires configuring an OAuth2 application in GCP + and registering your instance with it. + + ## Create an OAuth2 application in GCP + + Navigate to your project API & Services > Credentials to create a new OAuth2 application. + + ![integrations GCP secret manager config](../../images/integrations/gcp-secret-manager/integrations-gcp-secret-manager-config-api-services.png) + ![integrations GCP secret manager config](../../images/integrations/gcp-secret-manager/integrations-gcp-secret-manager-config-new-app.png) + + Create the application. As part of the form, add to **Authorized redirect URIs**: `https://your-domain.com/integrations/gitlab/oauth2/callback`. + + ![integrations GCP secret manager config](../../images/integrations/gcp-secret-manager/integrations-gcp-secret-manager-config-new-app-form.png) + + ## Add your OAuth2 application credentials to Infisical + + Obtain the **Client ID** and **Client Secret** for your GCP OAuth2 application. + + ![integrations GCP secret manager config](../../images/integrations/gcp-secret-manager/integrations-gcp-secret-manager-config-credentials.png) + + Back in your Infisical instance, add two new environment variables for the credentials of your GCP OAuth2 application: + + - `CLIENT_ID_GCP_SECRET_MANAGER`: The **Client ID** of your GCP OAuth2 application. + - `CLIENT_SECRET_GCP_SECRET_MANAGER`: The **Client Secret** of your GCP OAuth2 application. + + Once added, restart your Infisical instance and use the GCP Secret Manager integration. + + + + diff --git a/docs/integrations/cloud/heroku.mdx b/docs/integrations/cloud/heroku.mdx index f3c58d27e..bcac12f21 100644 --- a/docs/integrations/cloud/heroku.mdx +++ b/docs/integrations/cloud/heroku.mdx @@ -3,7 +3,9 @@ title: "Heroku" description: "How to sync secrets from Infisical to Heroku" --- -Prerequisites: + + + Prerequisites: - Set up and add envars to [Infisical Cloud](https://app.infisical.com) @@ -15,7 +17,7 @@ Prerequisites: Press on the Heroku tile and grant Infisical access to your Heroku account. -![integrations heroku authorization](../../images/integrations-heroku-auth.png) +![integrations heroku authorization](../../images/integrations/heroku/integrations-heroku-auth.png) If this is your project's first cloud integration, then you'll have to grant @@ -28,5 +30,38 @@ Press on the Heroku tile and grant Infisical access to your Heroku account. Select which Infisical environment secrets you want to sync to which Heroku app and press create integration to start syncing secrets to Heroku. -![integrations heroku](../../images/integrations-heroku-create.png) -![integrations heroku](../../images/integrations-heroku.png) +![integrations heroku](../../images/integrations/heroku/integrations-heroku-create.png) +![integrations heroku](../../images/integrations/heroku/integrations-heroku.png) + + + Using the Heroku integration on a self-hosted instance of Infisical requires configuring an API client in Heroku + and registering your instance with it. + + ## Create an API client in Heroku + + Navigate to your user Account settings > Applications to create a new API client. + + ![integrations Heroku config](../../images/integrations/heroku/integrations-heroku-config-settings.png) + ![integrations Heroku config](../../images/integrations/heroku/integrations-heroku-config-applications.png) + ![integrations Heroku config](../../images/integrations/heroku/integrations-heroku-config-new-app.png) + + Create the API client. As part of the form, set the **OAuth callback URL** to `https://your-domain.com/integrations/heroku/oauth2/callback`. + + ![integrations Heroku config](../../images/integrations/heroku/integrations-heroku-config-new-app-form.png) + + ## Add your Heroku API client credentials to Infisical + + Obtain the **Client ID** and **Client Secret** for your Heroku API client. + + ![integrations Heroku config](../../images/integrations/heroku/integrations-heroku-config-credentials.png) + + Back in your Infisical instance, add two new environment variables for the credentials of your Heroku API client. + + - `CLIENT_ID_HEROKU`: The **Client ID** of your Heroku API client. + - `CLIENT_SECRET_HEROKU`: The **Client Secret** of your Heroku API client. + + Once added, restart your Infisical instance and use the Heroku integration. + + + + diff --git a/docs/integrations/cloud/netlify.mdx b/docs/integrations/cloud/netlify.mdx index acd037417..6fe401021 100644 --- a/docs/integrations/cloud/netlify.mdx +++ b/docs/integrations/cloud/netlify.mdx @@ -3,7 +3,9 @@ title: "Netlify" description: "How to sync secrets from Infisical to Netlify" --- - + + + Infisical integrates with Netlify's new environment variable experience. If your site uses Netlify's old environment variable experience, you'll have to upgrade it to the new one to use this integration. @@ -21,7 +23,7 @@ Prerequisites: Press on the Netlify tile and grant Infisical access to your Netlify account. -![integrations netlify authorization](../../images/integrations-netlify-auth.png) +![integrations netlify authorization](../../images/integrations/netlify/integrations-netlify-auth.png) If this is your project's first cloud integration, then you'll have to grant @@ -34,5 +36,37 @@ Press on the Netlify tile and grant Infisical access to your Netlify account. Select which Infisical environment secrets you want to sync to which Netlify app and context. Lastly, press create integration to start syncing secrets to Netlify. -![integrations netlify](../../images/integrations-netlify-create.png) -![integrations netlify](../../images/integrations-netlify.png) +![integrations netlify](../../images/integrations/netlify/integrations-netlify-create.png) +![integrations netlify](../../images/integrations/netlify/integrations-netlify.png) + + + + Using the Netlify integration on a self-hosted instance of Infisical requires configuring an OAuth application in Netlify + and registering your instance with it. + + ## Create an OAuth application in Netlify + + Navigate to your User settings > Applications > OAuth to create a new OAuth application. + + ![integrations Netlify config](../../images/integrations/netlify/integrations-netlify-config-user-settings.png) + ![integrations Netlify config](../../images/integrations/netlify/integrations-netlify-config-new-app.png) + + Create the OAuth application. As part of the form, set the **Redirect URI** to `https://your-domain.com/integrations/netlify/oauth2/callback`. + + ![integrations Netlify config](../../images/integrations/netlify/integrations-netlify-config-new-app-form.png) + + ## Add your Netlify OAuth application credentials to Infisical + + Obtain the **Client ID** and **Secret** for your Netlify OAuth application. + + ![integrations Netlify config](../../images/integrations/netlify/integrations-netlify-config-credentials.png) + + Back in your Infisical instance, add two new environment variables for the credentials of your Netlify OAuth application. + + - `CLIENT_ID_NETLIFY`: The **Client ID** of your Netlify OAuth application. + - `CLIENT_SECRET_NETLIFY`: The **Secret** of your Netlify OAuth application. + + Once added, restart your Infisical instance and use the Netlify integration. + + + diff --git a/docs/integrations/cloud/teamcity.mdx b/docs/integrations/cloud/teamcity.mdx index 90661a790..2ade11a66 100644 --- a/docs/integrations/cloud/teamcity.mdx +++ b/docs/integrations/cloud/teamcity.mdx @@ -11,21 +11,18 @@ Prerequisites: ![integrations](../../images/integrations.png) -## Enter your TeamCity API Token and Server URL +## Enter your TeamCity Access Token and Server URL -Obtain a TeamCity API Token in Profile > Access Tokens +Obtain a TeamCity Access Token in Profile > Access Tokens -![integrations teamcity dashboard](../../images/integrations-teamcity-dashboard.png) -![integrations teamcity tokens](../../images/integrations-teamcity-tokens.png) +![integrations teamcity dashboard](../../images/integrations/teamcity/integrations-teamcity-dashboard.png) +![integrations teamcity token](../../images/integrations/teamcity/integrations-teamcity-token.png) -Obtain your TeamCity Server URL in Administration > Cloud Server Settings > Server URL - -![integrations teamcity projects](../../images/integrations-teamcity-projects.png) -![integrations teamcity server url](../../images/integrations-teamcity-serverurl.png) - -Press on the TeamCity tile and input your TeamCity API Token and Server URL to grant Infisical access to your TeamCity account. - -![integrations teamcity authorization](../../images/integrations-teamcity-auth.png) + + For this integration to work, the TeamCity Access Token must either have the + **Same as current user** account-wide permission enabled or, if **Limit per project** + is selected, then it must at minimum have the **View build configuration settings** and **Edit project** permissions enabled. + If this is your project's first cloud integration, then you'll have to grant @@ -34,9 +31,20 @@ Press on the TeamCity tile and input your TeamCity API Token and Server URL to g the cloud platform. +Press on the TeamCity tile and input your TeamCity Access Token and Server URL to grant Infisical access to your TeamCity account. + +![integrations teamcity authorization](../../images/integrations/teamcity/integrations-teamcity-auth.png) + ## Start integration -Select which Infisical environment secrets, you want to sync to which TeamCity project and press create integration to start syncing secrets to TeamCity. +Select which Infisical environment secrets you want to sync to which TeamCity project (and optionally build configuration) and press create integration to start syncing secrets to TeamCity. -![integrations teamcity](../../images/integrations-teamcity-create.png) -![integrations teamcity](../../images/integrations-teamcity.png) +![integrations teamcity](../../images/integrations/teamcity/integrations-teamcity-create.png) + + + Infisical integrates with both TeamCity's project-level and build configuration-level environment variables. + + To sync secrets to a specific build configuration in a TeamCity project, you can select a build configuration from the **TeamCity Build Config** dropdown; otherwise, leaving it empty will sync secrets to TeamCity at the project-level. + + +![integrations teamcity](../../images/integrations/teamcity/integrations-teamcity.png) diff --git a/docs/integrations/cloud/vercel.mdx b/docs/integrations/cloud/vercel.mdx index fb265ec98..f756e4fe8 100644 --- a/docs/integrations/cloud/vercel.mdx +++ b/docs/integrations/cloud/vercel.mdx @@ -3,7 +3,9 @@ title: "Vercel" description: "How to sync secrets from Infisical to Vercel" --- -Prerequisites: + + + Prerequisites: - Set up and add envars to [Infisical Cloud](https://app.infisical.com) @@ -15,7 +17,7 @@ Prerequisites: Press on the Vercel tile and grant Infisical access to your Vercel account. -![integrations vercel authorization](../../images/integrations-vercel-auth.png) +![integrations vercel authorization](../../images/integrations/vercel/integrations-vercel-auth.png) If this is your project's first cloud integration, then you'll have to grant @@ -28,8 +30,8 @@ Press on the Vercel tile and grant Infisical access to your Vercel account. Select which Infisical environment secrets you want to sync to which Vercel app and environment. Lastly, press create integration to start syncing secrets to Vercel. -![integrations vercel](../../images/integrations-vercel-create.png) -![integrations vercel](../../images/integrations-vercel.png) +![integrations vercel](../../images/integrations/vercel/integrations-vercel-create.png) +![integrations vercel](../../images/integrations/vercel/integrations-vercel.png) Infisical syncs every envar to Vercel with type `encrypted` unless an existing @@ -47,3 +49,37 @@ Select which Infisical environment secrets you want to sync to which Vercel app `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_SESSION_TOKEN`, `AWS_REGION`, and `AWS_DEFAULT_REGION`. + + + Using the Vercel integration on a self-hosted instance of Infisical requires configuring an integration in Vercel. + and registering your instance with it. + + ## Create an integration in Vercel + + Navigate to Integrations > Integration Console to create a new integration. + + ![integrations Vercel config](../../images/integrations/vercel/integrations-vercel-config-integrations-console.png) + ![integrations Vercel config](../../images/integrations/vercel/integrations-vercel-config-new-app.png) + + Create the application. As part of the form, set **Redirect URL** to `https://your-domain.com/integrations/vercel/oauth2/callback`. Also, + be sure to set the API Scopes according to the second screenshot below. + + ![integrations Vercel config](../../images/integrations/vercel/integrations-vercel-config-new-app-form-1.png) + ![integrations Vercel config](../../images/integrations/vercel/integrations-vercel-config-new-app-form-2.png) + + ## Add your Vercel integration credentials to Infisical + + Obtain the **Client (Integration) ID** and **Client (Integration) Secret** for your Vercel integration. + + ![integrations Vercel config](../../images/integrations/vercel/integrations-vercel-config-credentials.png) + + Back in your Infisical instance, add two new environment variables for the credentials of your Vercel integration. + + - `CLIENT_ID_VERCEL`: The **Client (Integration) ID** of your Vercel integration. + - `CLIENT_SECRET_VERCEL`: The **Client (Integration) Secret** of your Vercel integration. + + Once added, restart your Infisical instance and use the Vercel integration. + + + + diff --git a/docs/integrations/overview.mdx b/docs/integrations/overview.mdx index d97280361..a464aa9cb 100644 --- a/docs/integrations/overview.mdx +++ b/docs/integrations/overview.mdx @@ -31,6 +31,7 @@ Missing an integration? [Throw in a request](https://github.com/Infisical/infisi | [AWS Parameter Store](/integrations/cloud/aws-parameter-store) | Cloud | Available | | [AWS Secret Manager](/integrations/cloud/aws-secret-manager) | Cloud | Available | | [Azure Key Vault](/integrations/cloud/azure-key-vault) | Cloud | Available | +| [GCP Secret Manager](/integrations/cloud/gcp-secret-manager) | Cloud | Available | | [Windmill](/integrations/cloud/windmill) | Cloud | Available | | [BitBucket](/integrations/cicd/bitbucket) | CI/CD | Available | | [Codefresh](/integrations/cicd/codefresh) | CI/CD | Available | @@ -53,5 +54,4 @@ Missing an integration? [Throw in a request](https://github.com/Infisical/infisi | [Flask](/integrations/frameworks/flask) | Framework | Available | | [Laravel](/integrations/frameworks/laravel) | Framework | Available | | [Ruby on Rails](/integrations/frameworks/rails) | Framework | Available | -| GCP Secret Manager | Cloud | Coming soon | | Jenkins | CI/CD | Coming soon | diff --git a/docs/mint.json b/docs/mint.json index 4892def24..05bb4ee98 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -237,6 +237,7 @@ "integrations/cloud/checkly", "integrations/cloud/hashicorp-vault", "integrations/cloud/azure-key-vault", + "integrations/cloud/gcp-secret-manager", "integrations/cloud/cloud-66", "integrations/cloud/windmill", "integrations/cicd/githubactions", @@ -321,12 +322,21 @@ "api-reference/endpoints/workspaces/rollback-snapshot" ] }, + { + "group": "Environments", + "pages": [ + "api-reference/endpoints/environments/list", + "api-reference/endpoints/environments/create", + "api-reference/endpoints/environments/update", + "api-reference/endpoints/environments/delete" + ] + }, { "group": "Secrets", "pages": [ - "api-reference/endpoints/secrets/read", + "api-reference/endpoints/secrets/list", "api-reference/endpoints/secrets/create", - "api-reference/endpoints/secrets/read-one", + "api-reference/endpoints/secrets/read", "api-reference/endpoints/secrets/update", "api-reference/endpoints/secrets/delete", "api-reference/endpoints/secrets/versions", diff --git a/docs/self-hosting/deployment-options/kubernetes-helm.mdx b/docs/self-hosting/deployment-options/kubernetes-helm.mdx index 848b830de..83f3c8f6e 100644 --- a/docs/self-hosting/deployment-options/kubernetes-helm.mdx +++ b/docs/self-hosting/deployment-options/kubernetes-helm.mdx @@ -111,14 +111,14 @@ frontend: replicaCount: 2 image: repository: infisical/frontend - tag: "v0.1.3" + tag: "v0.26.0" # <--- frontend version pullPolicy: Always backend: replicaCount: 2 image: repository: infisical/backend - tag: "v0.1.3" + tag: "v0.26.0" # <--- backend version pullPolicy: Always backendEnvironmentVariables: @@ -126,7 +126,7 @@ backendEnvironmentVariables: ingress: nginx: - enabled: false #<-- if you would like to install nginx along with Infisical + enabled: true #<-- if you would like to install nginx along with Infisical ``` @@ -217,4 +217,5 @@ Once installation is complete, you will have to create the first account. No def ## Related blogs -- [Set up Infisical in a development cluster](https://iamunnip.hashnode.dev/infisical-open-source-secretops-kubernetes-setup) \ No newline at end of file +- [Set up Infisical in a development cluster](https://iamunnip.hashnode.dev/infisical-open-source-secretops-kubernetes-setup) +- [Set up Infisical in AKS using ArgoCD + Helm and integrate with an application using kustomize](https://mrdevops.medium.com/infisical-open-source-secretops-apply-it-using-gitops-approach-245f57fcd67e) diff --git a/docs/spec.yaml b/docs/spec.yaml index 799c7f6f4..5cf129be3 100644 --- a/docs/spec.yaml +++ b/docs/spec.yaml @@ -1949,6 +1949,8 @@ paths: properties: name: example: any + tagColor: + example: any slug: example: any /api/v2/workspace/tags/{tagId}: diff --git a/frontend/next.config.js b/frontend/next.config.js index b133818bd..3e9336f15 100644 --- a/frontend/next.config.js +++ b/frontend/next.config.js @@ -3,7 +3,7 @@ /** * @type {import('next').NextConfig} **/ -const path = require('path'); +const path = require("path"); const ContentSecurityPolicy = ` default-src 'self'; @@ -11,7 +11,7 @@ const ContentSecurityPolicy = ` style-src 'self' https://rsms.me 'unsafe-inline'; child-src https://api.stripe.com; frame-src https://js.stripe.com/ https://api.stripe.com https://www.youtube.com/; - connect-src 'self' wss://nexus-websocket-a.intercom.io https://api-iam.intercom.io https://api.heroku.com/ https://id.heroku.com/oauth/authorize https://id.heroku.com/oauth/token https://checkout.stripe.com https://app.posthog.com https://api.stripe.com http://localhost:*; + connect-src 'self' wss://nexus-websocket-a.intercom.io https://api-iam.intercom.io https://api.heroku.com/ https://id.heroku.com/oauth/authorize https://id.heroku.com/oauth/token https://checkout.stripe.com https://app.posthog.com https://api.stripe.com https://api.pwnedpasswords.com http://localhost:*; img-src 'self' https://static.intercomassets.com https://js.intercomcdn.com https://downloads.intercomcdn.com https://*.stripe.com https://i.ytimg.com/ data:; media-src https://js.intercomcdn.com; font-src 'self' https://fonts.intercomcdn.com/ https://maxcdn.bootstrapcdn.com https://rsms.me https://fonts.gstatic.com; @@ -21,50 +21,50 @@ const ContentSecurityPolicy = ` // after learning more below. const securityHeaders = [ { - key: 'X-DNS-Prefetch-Control', - value: 'on' + key: "X-DNS-Prefetch-Control", + value: "on" }, { - key: 'Strict-Transport-Security', - value: 'max-age=63072000; includeSubDomains; preload' + key: "Strict-Transport-Security", + value: "max-age=63072000; includeSubDomains; preload" }, { - key: 'X-XSS-Protection', - value: '1; mode=block' + key: "X-XSS-Protection", + value: "1; mode=block" }, { - key: 'X-Frame-Options', - value: 'SAMEORIGIN' + key: "X-Frame-Options", + value: "SAMEORIGIN" }, { - key: 'Permissions-Policy', - value: 'camera=(), microphone=()' + key: "Permissions-Policy", + value: "camera=(), microphone=()" }, { - key: 'X-Content-Type-Options', - value: 'nosniff' + key: "X-Content-Type-Options", + value: "nosniff" }, { - key: 'Referrer-Policy', - value: 'strict-origin-when-cross-origin' + key: "Referrer-Policy", + value: "strict-origin-when-cross-origin" }, { - key: 'Content-Security-Policy', - value: ContentSecurityPolicy.replace(/\s{2,}/g, ' ').trim() + key: "Content-Security-Policy", + value: ContentSecurityPolicy.replace(/\s{2,}/g, " ").trim() } ]; module.exports = { - output: 'standalone', + output: "standalone", i18n: { - locales: ['en', 'ko', 'fr', 'pt-BR', 'pt-PT', 'es'], - defaultLocale: 'en' + locales: ["en", "ko", "fr", "pt-BR", "pt-PT", "es"], + defaultLocale: "en" }, async headers() { return [ { // Apply these headers to all routes in your application. - source: '/:path*', + source: "/:path*", headers: securityHeaders } ]; @@ -73,15 +73,15 @@ module.exports = { // config config.module.rules.push({ test: /\.wasm$/, - loader: 'base64-loader', - type: 'javascript/auto' + loader: "base64-loader", + type: "javascript/auto" }); config.module.noParse = /\.wasm$/; config.module.rules.forEach((rule) => { (rule.oneOf || []).forEach((oneOf) => { - if (oneOf.loader && oneOf.loader.indexOf('file-loader') >= 0) { + if (oneOf.loader && oneOf.loader.indexOf("file-loader") >= 0) { oneOf.exclude.push(/\.wasm$/); } }); diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 568410bb2..990b03377 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -102,7 +102,7 @@ "@storybook/testing-library": "^0.2.0", "@tailwindcss/typography": "^0.5.4", "@types/jsrp": "^0.2.4", - "@types/node": "18.11.9", + "@types/node": "^18.11.9", "@types/react": "^18.0.26", "@types/sanitize-html": "^2.9.0", "@typescript-eslint/eslint-plugin": "^5.48.1", diff --git a/frontend/package.json b/frontend/package.json index 9aa355d4b..fab8b7033 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -110,7 +110,7 @@ "@storybook/testing-library": "^0.2.0", "@tailwindcss/typography": "^0.5.4", "@types/jsrp": "^0.2.4", - "@types/node": "18.11.9", + "@types/node": "^18.11.9", "@types/react": "^18.0.26", "@types/sanitize-html": "^2.9.0", "@typescript-eslint/eslint-plugin": "^5.48.1", diff --git a/frontend/public/data/frequentConstants.ts b/frontend/public/data/frequentConstants.ts index bcfaf1943..11adf2e7a 100644 --- a/frontend/public/data/frequentConstants.ts +++ b/frontend/public/data/frequentConstants.ts @@ -6,29 +6,30 @@ const integrationSlugNameMapping: Mapping = { "azure-key-vault": "Azure Key Vault", "aws-parameter-store": "AWS Parameter Store", "aws-secret-manager": "AWS Secret Manager", - heroku: "Heroku", - vercel: "Vercel", - netlify: "Netlify", - github: "GitHub", - gitlab: "GitLab", - render: "Render", + "heroku": "Heroku", + "vercel": "Vercel", + "netlify": "Netlify", + "github": "GitHub", + "gitlab": "GitLab", + "render": "Render", "laravel-forge": "Laravel Forge", - railway: "Railway", - flyio: "Fly.io", - circleci: "CircleCI", - travisci: "TravisCI", - supabase: "Supabase", - checkly: "Checkly", + "railway": "Railway", + "flyio": "Fly.io", + "circleci": "CircleCI", + "travisci": "TravisCI", + "supabase": "Supabase", + "checkly": "Checkly", "terraform-cloud": "Terraform Cloud", "teamcity": "TeamCity", "hashicorp-vault": "Vault", "cloudflare-pages": "Cloudflare Pages", "codefresh": "Codefresh", "digital-ocean-app-platform": "Digital Ocean App Platform", - bitbucket: "BitBucket", + "bitbucket": "BitBucket", "cloud-66": "Cloud 66", - northflank: "Northflank", - "windmill": "Windmill" + "northflank": "Northflank", + "windmill": "Windmill", + "gcp-secret-manager": "GCP Secret Manager" } const envMapping: Mapping = { diff --git a/frontend/public/locales/en/translations.json b/frontend/public/locales/en/translations.json index a70a7dd8b..f989f2f3a 100644 --- a/frontend/public/locales/en/translations.json +++ b/frontend/public/locales/en/translations.json @@ -231,10 +231,15 @@ "current": "Current password", "current-wrong": "The current password may be wrong", "new": "New password", - "validate-base": "Password should contain at least:", - "validate-length": "14 characters", - "validate-case": "1 lowercase character", - "validate-number": "1 number" + "validate-base": "Password should contain:", + "validate-tooShort": "at least 14 characters", + "validate-tooLong": "at most 100 characters", + "validate-noLetterChar": "at least 1 letter character", + "validate-noNumOrSpecialChar": "at least 1 number or special character", + "validate-repeatedChar": "at most 3 repeated, consecutive characters", + "validate-escapeChar": "No escape characters allowed.", + "validate-lowEntropy": "Password contains personal info.", + "validate-breached": "Password was found in a data breach." }, "token": { "service-tokens": "Service Tokens", diff --git a/frontend/public/locales/es/translations.json b/frontend/public/locales/es/translations.json index e734fb3f2..44da9a8ce 100644 --- a/frontend/public/locales/es/translations.json +++ b/frontend/public/locales/es/translations.json @@ -228,10 +228,15 @@ "current": "Contraseña actual", "current-wrong": "La contraseña actual puede puede que sea incorrecta", "new": "Nueva contraseña", - "validate-base": "La contraseña debe contener como mínimo:", - "validate-length": "14 caracteres", - "validate-case": "1 letra en minúsculas", - "validate-number": "1 número" + "validate-base": "La contraseña debe contener:", + "validate-tooShort": "al menos 14 caracteres", + "validate-tooLong": "como máximo 100 caracteres", + "validate-noLetterChar": "al menos 1 carácter alfabético", + "validate-noNumOrSpecialChar": "al menos 1 número o carácter especial", + "validate-repeatedChar": "como máximo 3 caracteres repetidos y consecutivos", + "validate-escapeChar": "No se permiten caracteres de escape.", + "validate-lowEntropy": "La contraseña contiene datos sensibles.", + "validate-breached": "La contraseña se encontró en una violación de datos." }, "token": { "service-tokens": "Tokens de servicio", diff --git a/frontend/public/locales/fr/translations.json b/frontend/public/locales/fr/translations.json index 6914e7ea1..60d5cf8cf 100644 --- a/frontend/public/locales/fr/translations.json +++ b/frontend/public/locales/fr/translations.json @@ -215,10 +215,15 @@ "current": "Mot de passe actuel", "current-wrong": "Le mot de passe actuel peut être érroné", "new": "Nouveau mot de passe", - "validate-base": "Le mot de passe doit contenir au moins:", - "validate-length": "14 caractères", - "validate-case": "1 caractère miniscule", - "validate-number": "1 chiffre" + "validate-base": "Le mot de passe doit contenir :", + "validate-tooShort": "au moins 14 caractères", + "validate-tooLong": "au plus 100 caractères", + "validate-noLetterChar": "au moins 1 caractère alphabétique", + "validate-noNumOrSpecialChar": "au moins 1 chiffre ou caractère spécial", + "validate-repeatedChar": "au plus 3 caractères consécutifs répétés", + "validate-escapeChar": "Aucun caractère d'échappement autorisé.", + "validate-lowEntropy": "Le mot de passe contient des données sensibles.", + "validate-breached": "Le mot de passe a été trouvé dans une violation de données." }, "token": { "service-tokens": "Jetons de service", @@ -296,4 +301,4 @@ "step5-subtitle": "Infisical a pour but d'être utilisé avec vos coéquipiers. Invitez-les à le tester.", "step5-skip": "Passer" } -} \ No newline at end of file +} diff --git a/frontend/public/locales/ko/translations.json b/frontend/public/locales/ko/translations.json index eea81f37e..e8169eaba 100644 --- a/frontend/public/locales/ko/translations.json +++ b/frontend/public/locales/ko/translations.json @@ -182,10 +182,15 @@ "current": "현재 비밀번호", "new": "새 비밀번호", "current-wrong": "현재 비밀번호가 잘못되었어요", - "validate-base": "비밀번호는 다음 조건을 만족해야 합니다:", - "validate-length": "14 글자 이상", - "validate-case": "1개 이상의 소문자", - "validate-number": "1개 이상의 숫자" + "validate-base": "비밀번호는 다음을 포함해야 합니다:", + "validate-tooShort": "최소 14자", + "validate-tooLong": "최대 100자", + "validate-noLetterChar": "최소 1개의 문자를 포함해야 합니다.", + "validate-noNumOrSpecialChar": "최소 1개의 숫자 또는 특수 문자를 포함해야 합니다.", + "validate-repeatedChar": "연속으로 최대 3개의 반복된 문자를 포함할 수 있습니다.", + "validate-escapeChar": "이스케이프 문자는 허용되지 않습니다.", + "validate-lowEntropy": "비밀번호에 민감한 데이터가 포함되어 있습니다.", + "validate-breached": "비밀번호가 데이터 유출에 포함되었습니다." }, "token": { "add-dialog": { @@ -256,4 +261,4 @@ "step4-description3": "분실시 접근하거나 복구할 수 없는 시크릿 키가 포함되어 있어요.", "step4-download": "PDF 다운로드" } -} \ No newline at end of file +} diff --git a/frontend/public/locales/pt-BR/translations.json b/frontend/public/locales/pt-BR/translations.json index 5b53ce503..ba324849b 100644 --- a/frontend/public/locales/pt-BR/translations.json +++ b/frontend/public/locales/pt-BR/translations.json @@ -210,10 +210,15 @@ "current": "Senha atual", "current-wrong": "A senha atual pode estar errada", "new": "Nova Senha", - "validate-base": "A senha deve conter pelo menos:", - "validate-length": "14 caracteres", - "validate-case": "1 caractere minúsculo", - "validate-number": "1 número" + "validate-base": "A senha deve conter:", + "validate-tooShort": "pelo menos 14 caracteres", + "validate-tooLong": "no máximo 100 caracteres", + "validate-noLetterChar": "pelo menos 1 caractere alfabético", + "validate-noNumOrSpecialChar": "pelo menos 1 número ou caractere especial", + "validate-repeatedChar": "no máximo 3 caracteres repetidos e consecutivos", + "validate-escapeChar": "Nenhum caractere de escape permitido.", + "validate-lowEntropy": "A senha contém dados sensíveis.", + "validate-breached": "A senha foi encontrada em uma violação de dados." }, "token": { "service-tokens": "Tokens de Serviço", @@ -290,4 +295,4 @@ "step5-subtitle": "Infisical foi feito para ser usado com seus colegas. Convide-os para testar também.", "step5-skip": "Pular" } -} \ No newline at end of file +} diff --git a/frontend/public/locales/tr/translations.json b/frontend/public/locales/tr/translations.json index 706998495..93f228f96 100644 --- a/frontend/public/locales/tr/translations.json +++ b/frontend/public/locales/tr/translations.json @@ -228,10 +228,15 @@ "current": "Mevcut şifre", "current-wrong": "Mevcut şifre yanlış olabilir", "new": "Yeni şifre", - "validate-base": "Şifre en az şunları içermelidir:", - "validate-length": "14 karakter", - "validate-case": "1 küçük harf", - "validate-number": "1 rakam" + "validate-base": "Parola içermelidir:", + "validate-tooShort": "en az 14 karakter", + "validate-tooLong": "en fazla 100 karakter", + "validate-noLetterChar": "en az 1 harf karakteri", + "validate-noNumOrSpecialChar": "en az 1 rakam veya özel karakter", + "validate-repeatedChar": "en fazla 3 tekrarlanan, ardışık karakter", + "validate-escapeChar": "Kaçış karakterlerine izin verilmez.", + "validate-lowEntropy": "Parola hassas veriler içeriyor.", + "validate-breached": "Parola veri ihlalinde bulundu." }, "token": { "service-tokens": "Servis Belirteçleri", diff --git a/frontend/src/components/AddTagPopoverContent/AddTagPopoverContent.tsx b/frontend/src/components/AddTagPopoverContent/AddTagPopoverContent.tsx new file mode 100644 index 000000000..a8b965269 --- /dev/null +++ b/frontend/src/components/AddTagPopoverContent/AddTagPopoverContent.tsx @@ -0,0 +1,78 @@ + +import { faPlus } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { Checkbox, PopoverContent } from "@app/components/v2"; + +import { WsTag } from "../../hooks/api/tags/types"; + +interface Props { + wsTags: WsTag[] | undefined; + secKey: string; + selectedTagIds: Record; + handleSelectTag: (wsTag: WsTag) => void; + handleTagOnMouseEnter: (wsTag: WsTag) => void; + handleTagOnMouseLeave: () => void; + checkIfTagIsVisible: (wsTag: WsTag) => boolean; + handleOnCreateTagOpen: () => void +} + +const AddTagPopoverContent = ({ + wsTags, + secKey, + selectedTagIds, + handleSelectTag, + handleTagOnMouseEnter, + handleTagOnMouseLeave, + checkIfTagIsVisible, + handleOnCreateTagOpen +}: Props) => { + return ( + +
+ Add tags to {secKey || "this secret"} +
+
+
+ {wsTags?.map((wsTag: WsTag) => ( +
handleSelectTag(wsTag)} + onMouseEnter={() => handleTagOnMouseEnter(wsTag)} + onMouseLeave={() => handleTagOnMouseLeave()} + tabIndex={0} role="button" + onKeyDown={() => { }}> + { + + (checkIfTagIsVisible(wsTag) || selectedTagIds?.[wsTag.slug]) && + } +
+
+ + {wsTag.slug} + +
+
+ ))} +
handleOnCreateTagOpen()} + tabIndex={0} role="button" + onKeyDown={() => { }}> + + Add new tag +
+
+ + ) +} + +export default AddTagPopoverContent \ No newline at end of file diff --git a/frontend/src/components/signup/UserInfoStep.tsx b/frontend/src/components/signup/UserInfoStep.tsx index db4d040ad..0e2b02f87 100644 --- a/frontend/src/components/signup/UserInfoStep.tsx +++ b/frontend/src/components/signup/UserInfoStep.tsx @@ -8,13 +8,12 @@ import jsrp from "jsrp"; import nacl from "tweetnacl"; import { encodeBase64 } from "tweetnacl-util"; -import { useGetCommonPasswords } from "@app/hooks/api"; import { completeAccountSignup } from "@app/hooks/api/auth/queries"; import { fetchOrganizations } from "@app/hooks/api/organization/queries"; import ProjectService from "@app/services/ProjectService"; import InputField from "../basic/InputField"; -import checkPassword from "../utilities/checks/checkPassword"; +import checkPassword from "../utilities/checks/password/checkPassword"; import Aes256Gcm from "../utilities/cryptography/aes-256-gcm"; import { deriveArgonKey } from "../utilities/cryptography/crypto"; import { saveTokenToLocalStorage } from "../utilities/saveTokenToLocalStorage"; @@ -39,12 +38,14 @@ interface UserInfoStepProps { } type Errors = { - length?: string, - upperCase?: string, - lowerCase?: string, - number?: string, - specialChar?: string, - repeatedChar?: string, + tooShort?: string; + tooLong?: string; + noLetterChar?: string; + noNumOrSpecialChar?: string; + repeatedChar?: string; + escapeChar?: string; + lowEntropy?: string; + breached?: string; }; /** @@ -71,9 +72,8 @@ export default function UserInfoStep({ setOrganizationName, attributionSource, setAttributionSource, - providerAuthToken, + providerAuthToken }: UserInfoStepProps): JSX.Element { - const { data: commonPasswords } = useGetCommonPasswords(); const [nameError, setNameError] = useState(false); const [organizationNameError, setOrganizationNameError] = useState(false); @@ -99,10 +99,9 @@ export default function UserInfoStep({ } else { setOrganizationNameError(false); } - - errorCheck = checkPassword({ + + errorCheck = await checkPassword({ password, - commonPasswords, setErrors }); @@ -174,7 +173,7 @@ export default function UserInfoStep({ salt: result.salt, verifier: result.verifier, organizationName, - attributionSource, + attributionSource }); // unset signup JWT token and set JWT token @@ -191,7 +190,7 @@ export default function UserInfoStep({ }); const userOrgs = await fetchOrganizations(); - + const orgId = userOrgs[0]?._id; const project = await ProjectService.initProject({ organizationId: orgId, @@ -215,13 +214,15 @@ export default function UserInfoStep({ }; return ( -
-

+

+

{t("signup.step3-message")}

-
-
-

Your Name

+
+
+

+ Your Name +

setName(e.target.value)} @@ -230,10 +231,16 @@ export default function UserInfoStep({ autoComplete="given-name" className="h-12" /> - {nameError &&

Please, specify your name

} + {nameError && ( +

+ Please, specify your name +

+ )}
-
-

Organization Name

+
+

+ Organization Name +

setOrganizationName(e.target.value)} @@ -241,10 +248,16 @@ export default function UserInfoStep({ isRequired className="h-12" /> - {organizationNameError &&

Please, specify your organization name

} + {organizationNameError && ( +

+ Please, specify your organization name +

+ )}
-
-

Where did you hear about us? (optional)

+
+

+ Where did you hear about us? (optional) +

setAttributionSource(e.target.value)} @@ -252,14 +265,13 @@ export default function UserInfoStep({ className="h-12" />
-
+
{ + onChangeHandler={async (pass: string) => { setPassword(pass); - checkPassword({ + await checkPassword({ password: pass, - commonPasswords, setErrors }); }} @@ -272,23 +284,20 @@ export default function UserInfoStep({ /> {Object.keys(errors).length > 0 && (
-
{t("section.password.validate-base")}
+
+ {t("section.password.validate-base")} +
{Object.keys(errors).map((key) => { if (errors[key as keyof Errors]) { return ( -
+
-
-

- {errors[key as keyof Errors]} -

+

{errors[key as keyof Errors]}

); } @@ -298,18 +307,21 @@ export default function UserInfoStep({
)}
-
-
+
+
+ > + {" "} + {String(t("signup.signup"))}{" "} +
diff --git a/frontend/src/components/utilities/checks/PasswordCheck.ts b/frontend/src/components/utilities/checks/PasswordCheck.ts deleted file mode 100644 index 5fb9dfe2c..000000000 --- a/frontend/src/components/utilities/checks/PasswordCheck.ts +++ /dev/null @@ -1,69 +0,0 @@ -/* eslint-disable no-param-reassign */ -interface PasswordCheckProps { - password: string; - errorCheck: boolean; - setPasswordErrorLength: (value: boolean) => void; - setPasswordErrorNumber: (value: boolean) => void; - setPasswordErrorLowerCase: (value: boolean) => void; -} - -/** - * This function checks a user password with respect to some criteria. - */ -const passwordCheck = ({ - password, - setPasswordErrorLength, - setPasswordErrorNumber, - setPasswordErrorLowerCase, - errorCheck -}: PasswordCheckProps) => { - - if (!password || password.length < 14) { - setPasswordErrorLength(true); - errorCheck = true; - } else { - setPasswordErrorLength(false); - } - - if (!/\d/.test(password)) { - setPasswordErrorNumber(true); - errorCheck = true; - } else { - setPasswordErrorNumber(false); - } - - if (!/[a-z]/.test(password)) { - setPasswordErrorLowerCase(true); - errorCheck = true; - // } else if (/(.)(?:(?!\1).){1,2}/.test(password)) { - // console.log(111) - // setPasswordError(true); - // setPasswordErrorMessage("Password should not contain repeating characters."); - // errorCheck = true; - // } else if (RegExp(`[${email}]`).test(password)) { - // console.log(222) - // setPasswordError(true); - // setPasswordErrorMessage("Password should not contain your email."); - // errorCheck = true; - } else { - setPasswordErrorLowerCase(false); - } - - // if (!/[A-Z]/.test(password)) { - // setPasswordErrorUpperCase(true); - // errorCheck = true; - // } else { - // setPasswordErrorUpperCase(false); - // } - - // if (!/(?=.*[!@#$%^&*])/.test(password)) { - // setPasswordErrorSpecialChar(true); - // // "Please add at least 1 special character (*, !, #, %)." - // errorCheck = true; - // } else { - // setPasswordErrorSpecialChar(false); - // } - return errorCheck; -}; - -export default passwordCheck; diff --git a/frontend/src/components/utilities/checks/checkPassword.ts b/frontend/src/components/utilities/checks/checkPassword.ts deleted file mode 100644 index 69dba2397..000000000 --- a/frontend/src/components/utilities/checks/checkPassword.ts +++ /dev/null @@ -1,72 +0,0 @@ -type Errors = { - length?: string, - upperCase?: string, - lowerCase?: string, - number?: string, - specialChar?: string, - repeatedChar?: string, - commonPassword?: string - }; - -interface CheckPasswordParams { - password: string; - commonPasswords: string[]; - setErrors: (value: Errors) => void; -} - -/** - * Validate that the password [password] is at least: - * - 8 characters long - * - Contains 1 uppercase character (A-Z) - * - Contains 1 lowercase character (a-z) - * - Contains 1 number (0-9) - * - Does not contain 3 repeat, consecutive characters - * - * The function returns whether or not the password [password] - * passes the minimum requirements above. It sets errors on - * an erorr object via [setErrors]. - * - * @param {Object} obj - * @param {String} obj.password - the password to check - * @param {Function} obj.setErrors - set state function to set error object - */ -const checkPassword = ({ - password, - commonPasswords, - setErrors -}: CheckPasswordParams): boolean => { - const errors: Errors = {}; - - if (password.length < 8) { - errors.length = "8 characters"; - } - - if (!/[A-Z]/.test(password)) { - errors.upperCase = "1 uppercase character (A-Z)"; - } - - if (!/[a-z]/.test(password)) { - errors.lowerCase = "1 lowercase character (a-z)"; - } - - if (!/[0-9]/.test(password)) { - errors.number = "1 number (0-9)"; - } - - if (!/[!@#$%^&*(),.?":{}|<>]/.test(password)) { - errors.specialChar = "1 special character (!@#$%^&*(),.?)"; - } - - if (/([A-Za-z0-9])\1\1\1/.test(password)) { - errors.repeatedChar = "No 3 repeat, consecutive characters"; - } - - if (commonPasswords.includes(password)) { - errors.commonPassword = "No common passwords"; - } - - setErrors(errors); - return Object.keys(errors).length > 0; -} - -export default checkPassword; \ No newline at end of file diff --git a/frontend/src/components/utilities/checks/password/PasswordCheck.ts b/frontend/src/components/utilities/checks/password/PasswordCheck.ts new file mode 100644 index 000000000..90ea4c7ea --- /dev/null +++ b/frontend/src/components/utilities/checks/password/PasswordCheck.ts @@ -0,0 +1,89 @@ +import { checkIsPasswordBreached } from "./checkIsPasswordBreached"; +import { escapeCharRegex, letterCharRegex, lowEntropyRegexes,numAndSpecialCharRegex, repeatedCharRegex } from "./passwordRegexes"; + +interface PasswordCheckProps { + password: string; + setPasswordErrorTooShort: (value: boolean) => void; + setPasswordErrorTooLong: (value: boolean) => void; + setPasswordErrorNoLetterChar: (value: boolean) => void; + setPasswordErrorNoNumOrSpecialChar: (value: boolean) => void; + setPasswordErrorRepeatedChar: (value: boolean) => void; + setPasswordErrorEscapeChar: (value: boolean) => void; + setPasswordErrorLowEntropy: (value: boolean) => void; + setPasswordErrorBreached: (value: boolean) => void; +} + +const passwordCheck = async ({ + password, + setPasswordErrorTooShort, + setPasswordErrorTooLong, + setPasswordErrorNoLetterChar, + setPasswordErrorNoNumOrSpecialChar, + setPasswordErrorRepeatedChar, + setPasswordErrorEscapeChar, + setPasswordErrorLowEntropy, + setPasswordErrorBreached +}: PasswordCheckProps) => { + let errorCheck = false; + const tests = [ + { + name: "tooShort", + validator: (pwd: string) => pwd.length >= 14, + setError: setPasswordErrorTooShort, + }, + { + name: "tooLong", + validator: (pwd: string) => pwd.length < 101, + setError: setPasswordErrorTooLong, + }, + { + name: "noLetterChar", + validator: (pwd: string) => letterCharRegex.test(pwd), + setError: setPasswordErrorNoLetterChar, + }, + { + name: "noNumOrSpecialChar", + validator: (pwd: string) => numAndSpecialCharRegex.test(pwd), + setError: setPasswordErrorNoNumOrSpecialChar, + }, + { + name: "repeatedChar", + validator: (pwd: string) => !repeatedCharRegex.test(pwd), + setError: setPasswordErrorRepeatedChar, + }, + { + name: "escapeChar", + validator: (pwd: string) => !escapeCharRegex.test(pwd), + setError: setPasswordErrorEscapeChar, + }, + { + name: "lowEntropy", + validator: (pwd: string) => ( + !lowEntropyRegexes.some(regex => regex.test(pwd)) + ), + setError: setPasswordErrorLowEntropy, + }, + ]; + + const isBreached = await checkIsPasswordBreached(password); + + if (isBreached) { + errorCheck = true; + setPasswordErrorBreached(true); + } else { + setPasswordErrorBreached(false); + } + + tests.forEach((test) => { + if (!test.validator(password)) { + errorCheck = true; + test.setError(true); + } else { + test.setError(false); + } + }) + + return errorCheck; +}; + +export default passwordCheck; diff --git a/frontend/src/components/utilities/checks/password/checkIsPasswordBreached.ts b/frontend/src/components/utilities/checks/password/checkIsPasswordBreached.ts new file mode 100644 index 000000000..d978441a6 --- /dev/null +++ b/frontend/src/components/utilities/checks/password/checkIsPasswordBreached.ts @@ -0,0 +1,113 @@ +import axios from "axios"; + +// SHA-1 hash the password using the SubtleCrypto API +async function hashPassword(passwordBytes: ArrayBuffer): Promise { + const buffer = await window.crypto.subtle.digest("SHA-1", passwordBytes); + return buffer; +} + +// Convert the hashed password buffer to a hexadecimal string +function bufferToHex(buffer: ArrayBuffer): string { + const byteArray = new Uint8Array(buffer); + const hexParts: string[] = []; + byteArray.forEach((byte) => { + const hex = byte.toString(16).padStart(2, "0"); + hexParts.push(hex); + }); + return hexParts.join(""); +} + + // see API details here: https://haveibeenpwned.com/API/v3#SearchingPwnedPasswordsByRange + // in short, the pending password is hashed (SHA-1), the first 5 chars are sliced and compared against a ranged hash table + // this hash table is formed from the 5 char hash prefix (ie. 00000-FFFFF) so 16^5 results + // returns a hash table of 800-1000 results + // padding has been added to prevent MitM attacker determining which hash table was called by the response size + // the last 35 chars of the password hash are compared client-side against the table + // if there is a match, that password has been involved in a password breach (ie. pwnd) and should NOT be accepted + // the database consists of ~700 mln breached passwords and is continuously updated, including with law enforcement ingestion + // https://www.troyhunt.com/open-source-pwned-passwords-with-fbi-feed-and-225m-new-nca-passwords-is-now-live/ + + // The HIBP API follows NIST guidance (pg.14) https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-63b.pdf + // "When processing requests to establish and change memorized secrets, verifiers SHALL compare + // the prospective secrets against a list that contains values known to be commonly-used, expected, + // or compromised. For example, the list MAY include, but is not limited to: + // • Passwords obtained from previous breach corpuses. + // • Dictionary words. + // • Repetitive or sequential characters (e.g. ‘aaaaaa’, ‘1234abcd’). + // • Context-specific words, such as the name of the service, the username, and derivatives + // thereof." + +export const checkIsPasswordBreached = async (password: string): Promise => { + const HAVE_I_BEEN_PWNED_API_URL = "https://api.pwnedpasswords.com"; + const maxRetryAttempts = 3; + + let encodedPwd: Uint8Array | undefined; + let hashedPwdBuffer: ArrayBuffer | undefined; + + try { + // Convert the password to a Uint8Array (UTF-8 encoded bytes) + const textEncoder = new TextEncoder(); + encodedPwd = textEncoder.encode(password); + + // Hash the password and convert it to a useful format for the HIBP API + hashedPwdBuffer = await hashPassword(encodedPwd!.buffer); + const hashedPwd = bufferToHex(hashedPwdBuffer).toUpperCase(); + // ONLY send the first 5 hash chars (over HTTPS) + const hashedPwdToSend = hashedPwd.slice(0, 5); + const safeHashedPwdToSend = encodeURIComponent(hashedPwdToSend); // Ensure URL safety + const rangedHashTableUri = `${HAVE_I_BEEN_PWNED_API_URL}/range/${safeHashedPwdToSend}`; + + let response; + let retryAttempt = 0; + + /* eslint-disable no-await-in-loop */ + while (retryAttempt < maxRetryAttempts) { + try { + response = await axios.get(rangedHashTableUri, { + headers: { + "Add-Padding": "true", // see https://www.troyhunt.com/enhancing-pwned-passwords-privacy-with-padding/ + "Content-Type": "text/plain", + }, + }); + + if (response.status === 200) { + // now we get back one of 16^5 hash prefix tables with random padding + const responseData = response.data.toUpperCase(); + // check the last 35 hash chars to see if there's a match + const isBreachedPassword: boolean = responseData.includes(hashedPwd.slice(5, 40)); + return isBreachedPassword; + } + retryAttempt += 1; + + } catch (err) { + if (!axios.isAxiosError(err)) { + throw err; + } + retryAttempt += 1; + } + } + + console.error( + `Received a non-200 response (${response ? response.status : "unknown"}) from the Pwnd Passwords API` + ); + return false; + } catch (err: any) { + console.error("An unexpected error has occurred:", err.message); + return false; + } finally { + + // Clear the UTF-8 encoded password from memory + + if (encodedPwd) { + const zeroEncodedPwdBuffer = new Uint8Array(encodedPwd.length); + encodedPwd.set(zeroEncodedPwdBuffer); + } + + // Clear the hashed password buffer from memory + + if (hashedPwdBuffer) { + const zeroHashedPwdBuffer = new Uint8Array(hashedPwdBuffer); + zeroHashedPwdBuffer.fill(0); + } + } +}; diff --git a/frontend/src/components/utilities/checks/password/checkPassword.ts b/frontend/src/components/utilities/checks/password/checkPassword.ts new file mode 100644 index 000000000..066303e1f --- /dev/null +++ b/frontend/src/components/utilities/checks/password/checkPassword.ts @@ -0,0 +1,99 @@ +import { checkIsPasswordBreached } from "./checkIsPasswordBreached"; +import { escapeCharRegex, letterCharRegex, lowEntropyRegexes,numAndSpecialCharRegex, repeatedCharRegex } from "./passwordRegexes"; + +type Errors = { + tooShort?: string; + tooLong?: string; + noLetterChar?: string; + noNumOrSpecialChar?: string; + repeatedChar?: string; + escapeChar?: string; + lowEntropy?: string; + breached?: string; +}; + +interface CheckPasswordParams { + password: string; + setErrors: (value: Errors) => void; +} + +/** + * Validate that the password [password]: + * - Contains at least 14 characters + * - Contains at most 100 characters + * - Contains at least 1 letter character (many languages supported) (case insensitive) + * - Contains at least 1 number (0-9) or special character (emojis included) + * - Does not contain 3 repeat, consecutive characters + * - Does not contain any escape characters/sequences + * - Does not contain PII and/or low entropy data (eg. email address, URL, phone number, DoB, SSN, driver's license, passport) + * - Is not in a database of breached passwords + * + * The function returns whether or not the password [password] + * passes the minimum requirements above. It sets errors on + * an erorr object via [setErrors]. + * + * @param {Object} obj + * @param {String} obj.password - the password to check + * @param {Function} obj.setErrors - set state function to set error object + */ + +const checkPassword = async ({ password, setErrors }: CheckPasswordParams): Promise => { + const errors: Errors = {}; + + const tests = [ + { + name: "tooShort", + validator: (pwd: string) => pwd.length >= 14, + errorText: "at least 14 characters", + }, + { + name: "tooLong", + validator: (pwd: string) => pwd.length < 101, + errorText: "at most 100 characters", + }, + { + name: "noLetterChar", + validator: (pwd: string) => letterCharRegex.test(pwd), + errorText: "at least 1 letter character", + }, + { + name: "noNumOrSpecialChar", + validator: (pwd: string) => numAndSpecialCharRegex.test(pwd), + errorText: "at least 1 number or special character", + }, + { + name: "repeatedChar", + validator: (pwd: string) => !repeatedCharRegex.test(pwd), + errorText: "at most 3 repeated, consecutive characters", + }, + { + name: "escapeChar", + validator: (pwd: string) => !escapeCharRegex.test(pwd), + errorText: "No escape characters allowed.", + }, + { + name: "lowEntropy", + validator: (pwd: string) => ( + !lowEntropyRegexes.some(regex => regex.test(pwd)) + ), + errorText: "Password contains personal info.", + }, + ]; + + const isBreached = await checkIsPasswordBreached(password); + + if (isBreached) { + errors.breached = "Password was found in a data breach."; + } + + tests.forEach((test) => { + if (test.validator && !test.validator(password)) { + errors[test.name as keyof Errors] = test.errorText; + } + }); + + setErrors(errors); + return Object.keys(errors).length > 0; +}; + +export default checkPassword; \ No newline at end of file diff --git a/frontend/src/components/utilities/checks/password/passwordRegexes.ts b/frontend/src/components/utilities/checks/password/passwordRegexes.ts new file mode 100644 index 000000000..c28d8da21 --- /dev/null +++ b/frontend/src/components/utilities/checks/password/passwordRegexes.ts @@ -0,0 +1,36 @@ +// This regex covers letters (case insensitive) for the top 50 most spoken languages +/* eslint-disable no-misleading-character-class */ +export const letterCharRegex = /[A-Za-z\u00C0-\u00D6\u00D8-\u00DE\u00DF-\u00F6\u00F8-\u00FF\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FFF\u0600-\u06FF\u0400-\u04FF\u0500-\u052F\u2DE0-\u2DFF\uA640-\uA69F\u05B0-\u05FF\u0980-\u09FF\u1F00-\u1FFF\u0130\u015E\u011E\u00C7\u00FC\u00FB\u00EB\u00E7]/u; + +// This regex covers digits, special characters, symbols, and emojis. +export const numAndSpecialCharRegex = /[\d!@#$%^&*(),.?":{}|<>]|[^\p{L}\p{N}\s]/u; + +// This regex covers 3 repeated consecutive chars (incl. spaces) +export const repeatedCharRegex = /(.)\1\1\1|\s{4,}/; + +// This regex covers the escape sequences as a precaution +export const escapeCharRegex = /[\n\t\r\\]/; + +// This regex covers some PII and/or low entropy data +export const lowEntropyRegexes = [ + // Email address + /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/, + + // URL (incl. subdomains, paths, top-level domains & query params) + /^(?:(?:https?|ftp):\/\/)?(?:\w+\.)?[a-zA-Z0-9.-]+\.(?:com|org|net|edu)(?:\/\S*)?(?:\?\S*)?$/, + + // Date in various formats + /(\b\d{1,4}[-/.]?\d{1,2}[-/.]?\d{1,4}\b)|(\b\d{1,4}[-/.]?\w{3}[-/.]?\d{1,4}\b)/, + + // Phone numbers (generalized) + /(?:\+(?:[1-9]\d{0,2})\s?)?(?:\(\d{1,4}\)\s?)?(?:\d[-.\s]?){5,}\d/, + + // Passport numbers (generalized) + /\b(?:[A-Z0-9]{6,9}|[A-Z0-9]{8,9}|[A-Z0-9]{9}|[A-Z0-9]{10,11})\b/, + + // Driver's license numbers (generalized) + /\b(?:[A-Z0-9]{7,10}|[A-Z0-9]{10,11}|[A-Z0-9]{7,10})\b/, + + // US social security number + /\b\d{3}[-\s]?\d{2}[-\s]?\d{4}\b/, +]; \ No newline at end of file diff --git a/frontend/src/components/utilities/isValidHexColor.ts b/frontend/src/components/utilities/isValidHexColor.ts new file mode 100644 index 000000000..86c14b142 --- /dev/null +++ b/frontend/src/components/utilities/isValidHexColor.ts @@ -0,0 +1,5 @@ +export const isValidHexColor = (hexColor: string) => { + const hexColorPattern = /^#?([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$/; + + return hexColorPattern.test(hexColor); +} \ No newline at end of file diff --git a/frontend/src/components/v2/Checkbox/Checkbox.tsx b/frontend/src/components/v2/Checkbox/Checkbox.tsx index 3c546f067..64ec0a54a 100644 --- a/frontend/src/components/v2/Checkbox/Checkbox.tsx +++ b/frontend/src/components/v2/Checkbox/Checkbox.tsx @@ -8,11 +8,12 @@ export type CheckboxProps = Omit< CheckboxPrimitive.CheckboxProps, "checked" | "disabled" | "required" > & { - children: ReactNode; + children?: ReactNode; id: string; isDisabled?: boolean; isChecked?: boolean; isRequired?: boolean; + checkIndicatorBg?: string | undefined; }; export const Checkbox = ({ @@ -22,6 +23,7 @@ export const Checkbox = ({ isChecked, isDisabled, isRequired, + checkIndicatorBg, ...props }: CheckboxProps): JSX.Element => { return ( @@ -39,7 +41,7 @@ export const Checkbox = ({ {...props} id={id} > - + diff --git a/frontend/src/components/v2/SecretInput/SecretInput.tsx b/frontend/src/components/v2/SecretInput/SecretInput.tsx index 5e517c08f..666c41d69 100644 --- a/frontend/src/components/v2/SecretInput/SecretInput.tsx +++ b/frontend/src/components/v2/SecretInput/SecretInput.tsx @@ -1,36 +1,35 @@ /* eslint-disable react/no-danger */ -import { HTMLAttributes } from "react"; +import { forwardRef, HTMLAttributes } from "react"; import ContentEditable from "react-contenteditable"; -import sanitizeHtml from "sanitize-html"; +import sanitizeHtml, { DisallowedTagsModes } from "sanitize-html"; import { useToggle } from "@app/hooks"; const REGEX = /\${([^}]+)}/g; -const stripSpanTags = (str: string) => str.replace(/<\/?span[^>]*>/g, ""); const replaceContentWithDot = (str: string) => { let finalStr = ""; - let isHtml = false; for (let i = 0; i < str.length; i += 1) { const char = str.at(i); - - if (char === "<" || char === ">") { - isHtml = char === "<"; - finalStr += char; - } else if (!isHtml && char !== "\n") { - finalStr += "•"; - } else { - finalStr += char; - } + finalStr += char === "\n" ? "\n" : "•"; } return finalStr; }; -const syntaxHighlight = (orgContent?: string | null, isVisible?: boolean) => { - if (orgContent === "") return "EMPTY"; - if (!orgContent) return "missing"; - if (!isVisible) return replaceContentWithDot(orgContent); - const content = stripSpanTags(orgContent); - const newContent = content.replace( +const sanitizeConf = { + allowedTags: ["span"], + disallowedTagsMode: "escape" as DisallowedTagsModes +}; + +const syntaxHighlight = (content?: string | null, isVisible?: boolean) => { + if (content === "") return "EMPTY"; + if (!content) return "missing"; + if (!isVisible) return replaceContentWithDot(content); + + const sanitizedContent = sanitizeHtml( + content.replaceAll("<", "<").replaceAll(">", ">"), + sanitizeConf + ); + const newContent = sanitizedContent.replace( REGEX, (_a, b) => `${${b}}` @@ -39,57 +38,58 @@ const syntaxHighlight = (orgContent?: string | null, isVisible?: boolean) => { return newContent; }; -const sanitizeConf = { - allowedTags: ["div", "span", "br", "p"] -}; - type Props = Omit, "onChange" | "onBlur"> & { value?: string | null; isVisible?: boolean; isDisabled?: boolean; - onChange?: (val: string, html: string) => void; - onBlur?: (sanitizedHtml: string) => void; + onChange?: (val: string) => void; + onBlur?: () => void; }; -export const SecretInput = ({ - value, - isVisible, - onChange, - onBlur, - isDisabled, - ...props -}: Props) => { - const [isSecretFocused, setIsSecretFocused] = useToggle(); +export const SecretInput = forwardRef( + ({ value, isVisible, onChange, onBlur, isDisabled, ...props }, ref) => { + const [isSecretFocused, setIsSecretFocused] = useToggle(); - return ( -
+ return (
- { - if (onChange) onChange(evt.currentTarget.innerText.trim(), evt.currentTarget.innerHTML); - }} - onFocus={() => setIsSecretFocused.on()} - disabled={isDisabled} - spellCheck={false} - onBlur={(evt) => { - if (onBlur) onBlur(sanitizeHtml(evt.currentTarget.innerHTML || "", sanitizeConf)); - setIsSecretFocused.off(); - }} - html={isVisible || isSecretFocused ? value || "" : syntaxHighlight(value, false)} - {...props} - /> -
- ); -}; + className="thin-scrollbar relative overflow-y-auto overflow-x-hidden" + style={{ maxHeight: `${21 * 7}px` }} + > +
+ { + if (onChange) onChange(evt.currentTarget.innerText.trim()); + }} + onFocus={() => setIsSecretFocused.on()} + disabled={isDisabled} + spellCheck={false} + onBlur={() => { + if (onBlur) onBlur(); + setIsSecretFocused.off(); + }} + html={ + isVisible || isSecretFocused + ? sanitizeHtml( + value?.replaceAll("<", "<").replaceAll(">", ">") || "", + sanitizeConf + ) + : syntaxHighlight(value, false) + } + {...props} + /> +
+ ); + } +); + +SecretInput.displayName = "SecretInput"; diff --git a/frontend/src/components/v2/Tag/Tag.tsx b/frontend/src/components/v2/Tag/Tag.tsx index 13672e7e9..10c1d6246 100644 --- a/frontend/src/components/v2/Tag/Tag.tsx +++ b/frontend/src/components/v2/Tag/Tag.tsx @@ -1,19 +1,14 @@ import { ReactNode } from "react"; -import { faClose } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { cva, VariantProps } from "cva"; import { twMerge } from "tailwind-merge"; type Props = { children: ReactNode; className?: string; - onClose?: () => void; - color?: string; - isDisabled?: boolean; } & VariantProps; const tagVariants = cva( - "inline-flex items-center whitespace-nowrap text-sm rounded-sm mr-1.5 text-bunker-200", + "inline-flex items-center whitespace-nowrap text-sm rounded-sm mr-1.5 text-bunker-200 rounded-[30px] text-gray-400 ", { variants: { colorSchema: { @@ -32,25 +27,10 @@ export const Tag = ({ children, className, colorSchema = "gray", - color, - isDisabled, - size = "sm", - onClose -}: Props) => ( + size = "sm" }: Props) => (
{children} - {onClose && ( - - )}
); diff --git a/frontend/src/const.ts b/frontend/src/const.ts index f2309df31..267d80ccd 100644 --- a/frontend/src/const.ts +++ b/frontend/src/const.ts @@ -51,3 +51,69 @@ const plansProd: Mapping = { export const plans = plansProd || plansDev; export const leaveConfirmDefaultMessage = "Your changes will be lost if you leave the page. Are you sure you want to continue?"; + +export const secretTagsColors = [ + { + id: 1, + hex: "#bec2c8", + rgba: "rgb(128,128,128, 0.8)", + name: "Grey", + selected: true + }, + { + id: 2, + hex: "#95a2b3", + rgba: "rgb(0,0,255, 0.8)", + name: "blue", + selected: false + }, + { + id: 3, + hex: "#5e6ad2", + rgba: "rgb(128,0,128, 0.8)", + name: "Purple", + selected: false + }, + { + id: 4, + hex: "#26b5ce", + rgba: "rgb(0,128,128, 0.8)", + name: "Teal", + selected: false + }, + { + id: 5, + hex: "#4cb782", + rgba: "rgb(0,128,0, 0.8)", + name: "Green", + selected: false + }, + { + id: 6, + hex: "#f2c94c", + rgba: "rgb(255,255,0, 0.8)", + name: "Yellow", + selected: false + }, + { + id: 7, + hex: "#f2994a", + rgba: "rgb(128,128,0, 0.8)", + name: "Orange", + selected: false + }, + { + id: 8, + hex: "#f7c8c1", + rgba: "rgb(128,0,0, 0.8)", + name: "Pink", + selected: false + }, + { + id: 9, + hex: "#eb5757", + rgba: "rgb(255,0,0, 0.8)", + name: "Red", + selected: false + }, +] \ No newline at end of file diff --git a/frontend/src/hooks/api/auth/index.tsx b/frontend/src/hooks/api/auth/index.tsx index dbcc77a5a..66208a487 100644 --- a/frontend/src/hooks/api/auth/index.tsx +++ b/frontend/src/hooks/api/auth/index.tsx @@ -1,10 +1,10 @@ export { useGetAuthToken, - useGetCommonPasswords, useResetPassword, - useSendMfaToken, + useSendMfaToken, useSendPasswordResetEmail, useSendVerificationEmail, useVerifyEmailVerificationCode, useVerifyMfaToken, - useVerifyPasswordResetCode} from "./queries" + useVerifyPasswordResetCode +} from "./queries"; diff --git a/frontend/src/hooks/api/auth/queries.tsx b/frontend/src/hooks/api/auth/queries.tsx index 094fd4b61..a26297730 100644 --- a/frontend/src/hooks/api/auth/queries.tsx +++ b/frontend/src/hooks/api/auth/queries.tsx @@ -20,22 +20,22 @@ import { SRPR1Res, VerifyMfaTokenDTO, VerifyMfaTokenRes, - VerifySignupInviteDTO} from "./types"; + VerifySignupInviteDTO +} from "./types"; const authKeys = { - getAuthToken: ["token"] as const, - commonPasswords: ["common-passwords"] as const + getAuthToken: ["token"] as const }; export const login1 = async (loginDetails: Login1DTO) => { const { data } = await apiRequest.post("/api/v3/auth/login1", loginDetails); return data; -} +}; export const login2 = async (loginDetails: Login2DTO) => { const { data } = await apiRequest.post("/api/v3/auth/login2", loginDetails); return data; -} +}; export const useLogin1 = () => { return useMutation({ @@ -47,7 +47,7 @@ export const useLogin1 = () => { return login1(details); } }); -} +}; export const useLogin2 = () => { return useMutation({ @@ -59,22 +59,22 @@ export const useLogin2 = () => { return login2(details); } }); -} +}; export const srp1 = async (details: SRP1DTO) => { const { data } = await apiRequest.post("/api/v1/password/srp1", details); - return data; -} + return data; +}; export const completeAccountSignup = async (details: CompleteAccountSignupDTO) => { const { data } = await apiRequest.post("/api/v3/signup/complete-account/signup", details); - return data; -} + return data; +}; export const completeAccountSignupInvite = async (details: CompleteAccountDTO) => { const { data } = await apiRequest.post("/api/v2/signup/complete-account/invite", details); - return data; -} + return data; +}; export const useCompleteAccountSignup = () => { return useMutation({ @@ -82,7 +82,7 @@ export const useCompleteAccountSignup = () => { return completeAccountSignup(details); } }); -} +}; export const useSendMfaToken = () => { return useMutation<{}, {}, SendMfaTokenDTO>({ @@ -91,22 +91,16 @@ export const useSendMfaToken = () => { return data; } }); -} +}; -export const verifyMfaToken = async ({ - email, - mfaCode -}: { - email: string; - mfaCode: string; -}) => { +export const verifyMfaToken = async ({ email, mfaCode }: { email: string; mfaCode: string }) => { const { data } = await apiRequest.post("/api/v2/auth/mfa/verify", { email, mfaToken: mfaCode }); return data; -} +}; export const useVerifyMfaToken = () => { return useMutation({ @@ -117,87 +111,67 @@ export const useVerifyMfaToken = () => { }); } }); -} +}; export const verifySignupInvite = async (details: VerifySignupInviteDTO) => { const { data } = await apiRequest.post("/api/v1/invite-org/verify", details); return data; -} +}; export const useSendVerificationEmail = () => { return useMutation({ - mutationFn: async ({ - email - }: { - email: string; - }) => { + mutationFn: async ({ email }: { email: string }) => { const { data } = await apiRequest.post("/api/v1/signup/email/signup", { email }); - + return data; } }); -} +}; export const useVerifyEmailVerificationCode = () => { return useMutation({ - mutationFn: async ({ - email, - code - }: { - email: string; - code: string; - }) => { + mutationFn: async ({ email, code }: { email: string; code: string }) => { const { data } = await apiRequest.post("/api/v1/signup/email/verify", { email, code }); - + return data; } }); -} +}; export const useSendPasswordResetEmail = () => { return useMutation({ - mutationFn: async ({ - email - }: { - email: string; - }) => { + mutationFn: async ({ email }: { email: string }) => { const { data } = await apiRequest.post("/api/v1/password/email/password-reset", { email }); - + return data; } }); -} +}; export const useVerifyPasswordResetCode = () => { return useMutation({ - mutationFn: async ({ - email, - code - }: { - email: string; - code: string; - }) => { + mutationFn: async ({ email, code }: { email: string; code: string }) => { const { data } = await apiRequest.post("/api/v1/password/email/password-reset-verify", { email, code }); - + return data; } }); -} +}; export const issueBackupPrivateKey = async (details: IssueBackupPrivateKeyDTO) => { const { data } = await apiRequest.post("/api/v1/password/backup-private-key", details); return data; -} +}; export const getBackupEncryptedPrivateKey = async ({ verificationToken @@ -207,37 +181,41 @@ export const getBackupEncryptedPrivateKey = async ({ Authorization: `Bearer ${verificationToken}` } }); - + return data.backupPrivateKey; -} +}; export const useResetPassword = () => { return useMutation({ mutationFn: async (details: ResetPasswordDTO) => { - const { data } = await apiRequest.post("/api/v1/password/password-reset", { - protectedKey: details.protectedKey, - protectedKeyIV: details.protectedKeyIV, - protectedKeyTag: details.protectedKeyTag, - encryptedPrivateKey: details.encryptedPrivateKey, - encryptedPrivateKeyIV: details.encryptedPrivateKeyIV, - encryptedPrivateKeyTag: details.encryptedPrivateKeyTag, - salt: details.salt, - verifier: details.verifier - }, { - headers: { - Authorization: `Bearer ${details.verificationToken}` + const { data } = await apiRequest.post( + "/api/v1/password/password-reset", + { + protectedKey: details.protectedKey, + protectedKeyIV: details.protectedKeyIV, + protectedKeyTag: details.protectedKeyTag, + encryptedPrivateKey: details.encryptedPrivateKey, + encryptedPrivateKeyIV: details.encryptedPrivateKeyIV, + encryptedPrivateKeyTag: details.encryptedPrivateKeyTag, + salt: details.salt, + verifier: details.verifier + }, + { + headers: { + Authorization: `Bearer ${details.verificationToken}` + } } - }); - + ); + return data; } }); -} +}; export const changePassword = async (details: ChangePasswordDTO) => { const { data } = await apiRequest.post("/api/v1/password/change-password", details); return data; -} +}; export const useChangePassword = () => { // note: use after srp1 @@ -246,7 +224,7 @@ export const useChangePassword = () => { return changePassword(details); } }); -} +}; // Refresh token is set as cookie when logged in // Using that we fetch the auth bearer token needed for auth calls @@ -263,11 +241,3 @@ export const useGetAuthToken = () => onSuccess: (data) => setAuthToken(data.token), retry: 0 }); - -const fetchCommonPasswords = async () => { - const { data } = await apiRequest.get("/api/v1/auth/common-passwords"); - return data || []; -}; - -export const useGetCommonPasswords = () => - useQuery({ queryKey: authKeys.commonPasswords, queryFn: fetchCommonPasswords }); \ No newline at end of file diff --git a/frontend/src/hooks/api/integrationAuth/index.tsx b/frontend/src/hooks/api/integrationAuth/index.tsx index d6e1b11e3..7e7c355ca 100644 --- a/frontend/src/hooks/api/integrationAuth/index.tsx +++ b/frontend/src/hooks/api/integrationAuth/index.tsx @@ -7,6 +7,7 @@ export { useGetIntegrationAuthNorthflankSecretGroups, useGetIntegrationAuthRailwayEnvironments, useGetIntegrationAuthRailwayServices, + useGetIntegrationAuthTeamCityBuildConfigs, useGetIntegrationAuthTeams, useGetIntegrationAuthVercelBranches, useSaveIntegrationAccessToken diff --git a/frontend/src/hooks/api/integrationAuth/queries.tsx b/frontend/src/hooks/api/integrationAuth/queries.tsx index 1ec08d839..ec0c785f2 100644 --- a/frontend/src/hooks/api/integrationAuth/queries.tsx +++ b/frontend/src/hooks/api/integrationAuth/queries.tsx @@ -10,8 +10,8 @@ import { IntegrationAuth, NorthflankSecretGroup, Service, - Team -} from "./types"; + Team, + TeamCityBuildConfig} from "./types"; const integrationAuthKeys = { getIntegrationAuthById: (integrationAuthId: string) => @@ -49,7 +49,14 @@ const integrationAuthKeys = { }: { integrationAuthId: string; appId: string; - }) => [{ integrationAuthId, appId }, "integrationAuthNorthflankSecretGroups"] as const, + }) => [{ integrationAuthId, appId }, "integrationAuthNorthflankSecretGroups"] as const, + getIntegrationAuthTeamCityBuildConfigs: ({ + integrationAuthId, + appId + }: { + integrationAuthId: string; + appId: string; + }) => [{ integrationAuthId, appId }, "integrationAuthTeamCityBranchConfigs"] as const, }; const fetchIntegrationAuthById = async (integrationAuthId: string) => { @@ -183,6 +190,27 @@ const fetchIntegrationAuthNorthflankSecretGroups = async ({ return secretGroups; }; +const fetchIntegrationAuthTeamCityBuildConfigs = async ({ + integrationAuthId, + appId +}: { + integrationAuthId: string; + appId: string; +}) => { + const { + data: { buildConfigs } + } = await apiRequest.get<{ buildConfigs: TeamCityBuildConfig[] }>( + `/api/v1/integration-auth/${integrationAuthId}/teamcity/build-configs`, + { + params: { + appId + } + } + ); + + return buildConfigs; +}; + export const useGetIntegrationAuthById = (integrationAuthId: string) => { return useQuery({ queryKey: integrationAuthKeys.getIntegrationAuthById(integrationAuthId), @@ -312,6 +340,26 @@ export const useGetIntegrationAuthNorthflankSecretGroups = ({ }); }; +export const useGetIntegrationAuthTeamCityBuildConfigs = ({ + integrationAuthId, + appId +}: { + integrationAuthId: string; + appId: string; +}) => { + return useQuery({ + queryKey: integrationAuthKeys.getIntegrationAuthTeamCityBuildConfigs({ + integrationAuthId, + appId + }), + queryFn: () => fetchIntegrationAuthTeamCityBuildConfigs({ + integrationAuthId, + appId + }), + enabled: true + }); +}; + export const useAuthorizeIntegration = () => { const queryClient = useQueryClient(); diff --git a/frontend/src/hooks/api/integrationAuth/types.ts b/frontend/src/hooks/api/integrationAuth/types.ts index 1214600e8..47f0fcfc9 100644 --- a/frontend/src/hooks/api/integrationAuth/types.ts +++ b/frontend/src/hooks/api/integrationAuth/types.ts @@ -40,4 +40,9 @@ export type BitBucketWorkspace = { export type NorthflankSecretGroup = { name: string; groupId: string; +} + +export type TeamCityBuildConfig = { + name: string; + buildConfigId: string; } \ No newline at end of file diff --git a/frontend/src/hooks/api/integrations/queries.tsx b/frontend/src/hooks/api/integrations/queries.tsx index bfca2f407..6e7b65eed 100644 --- a/frontend/src/hooks/api/integrations/queries.tsx +++ b/frontend/src/hooks/api/integrations/queries.tsx @@ -40,7 +40,8 @@ export const useCreateIntegration = () => { owner, path, region, - secretPath + secretPath, + metadata }: { integrationAuthId: string; isActive: boolean; @@ -55,6 +56,9 @@ export const useCreateIntegration = () => { owner: string | null; path: string | null; region: string | null; + metadata?: { + secretSuffix?: string; + } }) => { const { data: { integration } } = await apiRequest.post("/api/v1/integration", { integrationAuthId, @@ -69,7 +73,8 @@ export const useCreateIntegration = () => { owner, path, region, - secretPath + secretPath, + metadata }); return integration; diff --git a/frontend/src/hooks/api/integrations/types.ts b/frontend/src/hooks/api/integrations/types.ts index 5ff2bbac7..199d02975 100644 --- a/frontend/src/hooks/api/integrations/types.ts +++ b/frontend/src/hooks/api/integrations/types.ts @@ -30,4 +30,7 @@ export type TIntegration = { createdAt: string; updatedAt: string; __v: number; + metadata?: { + secretSuffix?: string; + } }; diff --git a/frontend/src/hooks/api/secrets/queries.tsx b/frontend/src/hooks/api/secrets/queries.tsx index e1c3e10bf..cd032e58b 100644 --- a/frontend/src/hooks/api/secrets/queries.tsx +++ b/frontend/src/hooks/api/secrets/queries.tsx @@ -38,7 +38,7 @@ const fetchProjectEncryptedSecrets = async ( folderId?: string, secretPath?: string ) => { - const { data } = await apiRequest.get<{ secrets: EncryptedSecret[] }>("/api/v2/secrets", { + const { data } = await apiRequest.get<{ secrets: EncryptedSecret[] }>("/api/v3/secrets", { params: { environment: env, workspaceId, @@ -46,6 +46,7 @@ const fetchProjectEncryptedSecrets = async ( secretPath } }); + return data.secrets; }; @@ -344,4 +345,4 @@ export const useCreateSecret = () => { ); } }); -}; \ No newline at end of file +}; diff --git a/frontend/src/hooks/api/tags/queries.tsx b/frontend/src/hooks/api/tags/queries.tsx index 74900da0e..7c230e02a 100644 --- a/frontend/src/hooks/api/tags/queries.tsx +++ b/frontend/src/hooks/api/tags/queries.tsx @@ -7,7 +7,7 @@ import { CreateTagRes, DeleteTagDTO, DeleteWsTagRes, - UserWsTags + UserWsTags, } from "./types"; const workspaceTags = { @@ -30,13 +30,15 @@ export const useGetWsTags = (workspaceID: string) => { }); } + export const useCreateWsTag = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async ({ workspaceID, tagName, tagSlug }) => { + mutationFn: async ({ workspaceID, tagName, tagColor, tagSlug }) => { const { data } = await apiRequest.post(`/api/v2/workspace/${workspaceID}/tags`, { name: tagName, + tagColor: tagColor || "", slug: tagSlug }) return data; @@ -47,6 +49,7 @@ export const useCreateWsTag = () => { }); }; + export const useDeleteWsTag = () => { const queryClient = useQueryClient(); diff --git a/frontend/src/hooks/api/tags/types.ts b/frontend/src/hooks/api/tags/types.ts index 56b7171bf..de09f5ac4 100644 --- a/frontend/src/hooks/api/tags/types.ts +++ b/frontend/src/hooks/api/tags/types.ts @@ -4,6 +4,7 @@ export type WsTag = { _id: string; name: string; slug: string; + tagColor?: string; workspace: string; createdAt: string; updatedAt: string; @@ -16,6 +17,7 @@ export type CreateTagDTO = { workspaceID: string; tagSlug: string; tagName: string; + tagColor: string; }; export type CreateTagRes = { @@ -23,6 +25,7 @@ export type CreateTagRes = { slug: string; workspace: string; createdAt: string; + tagColor?: string; user: string; _id: string; }; @@ -36,4 +39,19 @@ export type DeleteWsTagRes = { createdAt: string; user: string; _id: string; -}; \ No newline at end of file +}; + +export type SecretTags = { + id: string; + _id: string; + slug: string; + tagColor: string; +} + +export type TagColor = { + id: number; + hex: string + rgba: string + name: string + selected: boolean +} \ No newline at end of file diff --git a/frontend/src/pages/integrations/checkly/create.tsx b/frontend/src/pages/integrations/checkly/create.tsx index 7f3b5397b..6b3febfb8 100644 --- a/frontend/src/pages/integrations/checkly/create.tsx +++ b/frontend/src/pages/integrations/checkly/create.tsx @@ -35,6 +35,7 @@ export default function ChecklyCreateIntegrationPage() { const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(""); const [secretPath, setSecretPath] = useState("/"); + const [secretSuffix, setSecretSuffix] = useState(""); const [targetApp, setTargetApp] = useState(""); const [targetAppId, setTargetAppId] = useState(""); @@ -78,7 +79,10 @@ export default function ChecklyCreateIntegrationPage() { owner: null, path: null, region: null, - secretPath + secretPath, + metadata: { + secretSuffix + } }); setIsLoading(false); @@ -148,6 +152,13 @@ export default function ChecklyCreateIntegrationPage() { )} + + setSecretSuffix(evt.target.value)} + placeholder="Provide a suffix for secret names, default is no suffix" + /> + + +
+ ) : ( +
+ ); +} + +GCPSecretManagerCreateIntegrationPage.requireAuth = true; diff --git a/frontend/src/pages/integrations/gcp-secret-manager/oauth2/callback.tsx b/frontend/src/pages/integrations/gcp-secret-manager/oauth2/callback.tsx new file mode 100644 index 000000000..740897496 --- /dev/null +++ b/frontend/src/pages/integrations/gcp-secret-manager/oauth2/callback.tsx @@ -0,0 +1,38 @@ +import { useEffect } from "react"; +import { useRouter } from "next/router"; +import queryString from "query-string"; + +import { + useAuthorizeIntegration +} from "@app/hooks/api"; + +export default function GCPSecretManagerOAuth2CallbackPage() { + const router = useRouter(); + const { mutateAsync } = useAuthorizeIntegration(); + + const { code, state } = queryString.parse(router.asPath.split("?")[1]); + + useEffect(() => { + (async () => { + try { + // validate state + + if (state !== localStorage.getItem("latestCSRFToken")) return; + localStorage.removeItem("latestCSRFToken"); + const integrationAuth = await mutateAsync({ + workspaceId: localStorage.getItem("projectData.id") as string, + code: code as string, + integration: "gcp-secret-manager" + }); + + router.push(`/integrations/gcp-secret-manager/create?integrationAuthId=${integrationAuth._id}`); + } catch (err) { + console.error(err); + } + })(); + }, []); + + return
; +} + +GCPSecretManagerOAuth2CallbackPage.requireAuth = true; diff --git a/frontend/src/pages/integrations/teamcity/create.tsx b/frontend/src/pages/integrations/teamcity/create.tsx index a6282af92..e2a2b2d58 100644 --- a/frontend/src/pages/integrations/teamcity/create.tsx +++ b/frontend/src/pages/integrations/teamcity/create.tsx @@ -17,13 +17,20 @@ import { } from "../../../components/v2"; import { useGetIntegrationAuthApps, - useGetIntegrationAuthById + useGetIntegrationAuthById, + useGetIntegrationAuthTeamCityBuildConfigs } from "../../../hooks/api/integrationAuth"; import { useGetWorkspaceById } from "../../../hooks/api/workspace"; export default function TeamCityCreateIntegrationPage() { const router = useRouter(); const { mutateAsync } = useCreateIntegration(); + + const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(""); + const [targetAppId, setTargetAppId] = useState(""); + const [targetBuildConfigId, setTargetBuildConfigId] = useState(""); + const [secretPath, setSecretPath] = useState("/"); + const [isLoading, setIsLoading] = useState(false); const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]); @@ -32,11 +39,11 @@ export default function TeamCityCreateIntegrationPage() { const { data: integrationAuthApps } = useGetIntegrationAuthApps({ integrationAuthId: (integrationAuthId as string) ?? "" }); - - const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(""); - const [targetApp, setTargetApp] = useState(""); - const [secretPath, setSecretPath] = useState("/"); - const [isLoading, setIsLoading] = useState(false); + + const { data: targetBuildConfigs } = useGetIntegrationAuthTeamCityBuildConfigs({ + integrationAuthId: (integrationAuthId as string) ?? "", + appId: targetAppId + }); useEffect(() => { if (workspace) { @@ -47,29 +54,31 @@ export default function TeamCityCreateIntegrationPage() { useEffect(() => { if (integrationAuthApps) { if (integrationAuthApps.length > 0) { - setTargetApp(integrationAuthApps[0].name); + setTargetAppId(integrationAuthApps[0].appId as string); } else { - setTargetApp("none"); + setTargetAppId("none"); } } }, [integrationAuthApps]); - + const handleButtonClick = async () => { try { if (!integrationAuth?._id) return; setIsLoading(true); - + + const targetEnvironment = targetBuildConfigs?.find( + (buildConfig) => buildConfig.buildConfigId === targetBuildConfigId + ); + await mutateAsync({ integrationAuthId: integrationAuth?._id, isActive: true, - app: targetApp, - appId: - integrationAuthApps?.find((integrationAuthApp) => integrationAuthApp.name === targetApp) - ?.appId ?? null, + app: integrationAuthApps?.find((integrationAuthApp) => integrationAuthApp.appId === targetAppId)?.name ?? null, + appId: targetAppId, sourceEnvironment: selectedSourceEnvironment, - targetEnvironment: null, - targetEnvironmentId: null, + targetEnvironment: targetEnvironment ? targetEnvironment.name : null, + targetEnvironmentId: targetEnvironment ? targetEnvironment.buildConfigId : null, targetService: null, targetServiceId: null, owner: null, @@ -86,12 +95,17 @@ export default function TeamCityCreateIntegrationPage() { } }; + const filteredBuildConfigs = targetBuildConfigs?.concat({ + name: "", + buildConfigId: "" + }); return integrationAuth && workspace && selectedSourceEnvironment && integrationAuthApps && - targetApp ? ( + filteredBuildConfigs && + targetAppId ? (
TeamCity Integration @@ -120,16 +134,16 @@ export default function TeamCityCreateIntegrationPage() { + + +
@@ -205,13 +213,16 @@ export default function PasswordReset() { // Enter new password const stepEnterNewPassword = ( -
+

Enter new password

- Make sure you save it somewhere save. + Make sure you save it somewhere safe.

@@ -221,55 +232,141 @@ export default function PasswordReset() { setNewPassword(password); passwordCheck({ password, - setPasswordErrorLength, - setPasswordErrorNumber, - setPasswordErrorLowerCase, - errorCheck: false + setPasswordErrorTooShort, + setPasswordErrorTooLong, + setPasswordErrorNoLetterChar, + setPasswordErrorNoNumOrSpecialChar, + setPasswordErrorRepeatedChar, + setPasswordErrorEscapeChar, + setPasswordErrorLowEntropy, + setPasswordErrorBreached }); }} type="password" value={newPassword} isRequired - error={passwordErrorLength && passwordErrorLowerCase && passwordErrorNumber} + error={ + passwordErrorTooShort && + passwordErrorTooLong && + passwordErrorNoLetterChar && + passwordErrorNoNumOrSpecialChar && + passwordErrorRepeatedChar && + passwordErrorEscapeChar && + passwordErrorLowEntropy && + passwordErrorBreached + } autoComplete="new-password" id="new-password" />
- {passwordErrorLength || passwordErrorLowerCase || passwordErrorNumber ? ( + {passwordErrorTooShort || + passwordErrorTooLong || + passwordErrorNoLetterChar || + passwordErrorNoNumOrSpecialChar || + passwordErrorRepeatedChar || + passwordErrorEscapeChar || + passwordErrorLowEntropy || + passwordErrorBreached ? (
-
Password should contain at least:
+
Password should contain:
- {passwordErrorLength ? ( + {passwordErrorTooShort ? ( ) : ( )} -
- 14 characters +
+ at least 14 characters
- {passwordErrorLowerCase ? ( + {passwordErrorTooLong ? ( + + ) : ( + + )} +
+ at most 100 characters +
+
+
+ {passwordErrorNoLetterChar ? ( ) : ( )}
- 1 lowercase character + at least 1 letter character
- {passwordErrorNumber ? ( + {passwordErrorNoNumOrSpecialChar ? ( ) : ( )} -
- 1 number +
+ at least 1 number or special character
+
+ {passwordErrorRepeatedChar ? ( + + ) : ( + + )} +
+ at most 3 repeated, consecutive characters +
+
+
+ {passwordErrorEscapeChar ? ( + + ) : ( + + )} +
+ No escape characters allowed. +
+
+
+ {passwordErrorLowEntropy ? ( + + ) : ( + + )} +
+ Password contains personal info. +
+
+
+ {passwordErrorBreached ? ( + + ) : ( + + )} +
+ Password was found in a data breach. +
+
) : (
diff --git a/frontend/src/pages/signupinvite.tsx b/frontend/src/pages/signupinvite.tsx index 9b1fe990f..0784806c8 100644 --- a/frontend/src/pages/signupinvite.tsx +++ b/frontend/src/pages/signupinvite.tsx @@ -16,36 +16,30 @@ import { encodeBase64 } from "tweetnacl-util"; import Button from "@app/components/basic/buttons/Button"; import InputField from "@app/components/basic/InputField"; -import checkPassword from "@app/components/utilities/checks/checkPassword"; +import checkPassword from "@app/components/utilities/checks/password/checkPassword"; import Aes256Gcm from "@app/components/utilities/cryptography/aes-256-gcm"; import { deriveArgonKey } from "@app/components/utilities/cryptography/crypto"; import issueBackupKey from "@app/components/utilities/cryptography/issueBackupKey"; import { saveTokenToLocalStorage } from "@app/components/utilities/saveTokenToLocalStorage"; import SecurityClient from "@app/components/utilities/SecurityClient"; -import { - useGetCommonPasswords -} from "@app/hooks/api"; -import { - completeAccountSignupInvite, - verifySignupInvite -} from "@app/hooks/api/auth/queries"; +import { completeAccountSignupInvite, verifySignupInvite } from "@app/hooks/api/auth/queries"; import { fetchOrganizations } from "@app/hooks/api/organization/queries"; // eslint-disable-next-line new-cap const client = new jsrp.client(); type Errors = { - length?: string, - upperCase?: string, - lowerCase?: string, - number?: string, - specialChar?: string, - repeatedChar?: string, + tooShort?: string; + tooLong?: string; + noLetterChar?: string; + noNumOrSpecialChar?: string; + repeatedChar?: string; + escapeChar?: string; + lowEntropy?: string; + breached?: string; }; export default function SignupInvite() { - const { data: commonPasswords } = useGetCommonPasswords(); - const [password, setPassword] = useState(""); const [firstName, setFirstName] = useState(""); const [lastName, setLastName] = useState(""); @@ -79,10 +73,9 @@ export default function SignupInvite() { } else { setLastNameError(false); } - - errorCheck = checkPassword({ + + errorCheck = await checkPassword({ password, - commonPasswords, setErrors }); @@ -116,7 +109,7 @@ export default function SignupInvite() { if (!derivedKey) throw new Error("Failed to derive key from password"); const key = crypto.randomBytes(32); - + // create encrypted private key by encrypting the private // key with the symmetric key [key] const { @@ -127,7 +120,7 @@ export default function SignupInvite() { text: privateKey, secret: key }); - + // create the protected key by encrypting the symmetric key // [key] with the derived key const { @@ -138,10 +131,8 @@ export default function SignupInvite() { text: key.toString("hex"), secret: Buffer.from(derivedKey.hash) }); - - const { - token: jwtToken - } = await completeAccountSignupInvite({ + + const { token: jwtToken } = await completeAccountSignupInvite({ email, firstName, lastName, @@ -155,20 +146,20 @@ export default function SignupInvite() { salt: result.salt, verifier: result.verifier }); - + // unset temporary signup JWT token and set JWT token SecurityClient.setSignupToken(""); SecurityClient.setToken(jwtToken); saveTokenToLocalStorage({ - publicKey, - encryptedPrivateKey, - iv: encryptedPrivateKeyIV, - tag: encryptedPrivateKeyTag, - privateKey + publicKey, + encryptedPrivateKey, + iv: encryptedPrivateKeyIV, + tag: encryptedPrivateKeyTag, + privateKey }); - const userOrgs = await fetchOrganizations(); + const userOrgs = await fetchOrganizations(); const orgId = userOrgs[0]._id; localStorage.setItem("orgData.id", orgId); @@ -188,12 +179,12 @@ export default function SignupInvite() { // Step 4 of the sign up process (download the emergency kit pdf) const stepConfirmEmail = ( -
-

+

+

Confirm your email

verify email -
+
- ))} - -
- + onSelectTag(wsTag)} + handleTagOnMouseEnter={(wsTag: WsTag) => handleTagOnMouseEnter(wsTag)} + handleTagOnMouseLeave={() => handleTagOnMouseLeave()} + checkIfTagIsVisible={(wsTag: WsTag) => checkIfTagIsVisible(wsTag)} + handleOnCreateTagOpen={() => onCreateTagOpen()} + />
)} @@ -460,20 +421,16 @@ export const SecretInputRow = memo( - - -