From 65afaa8177d3a55c799640fd72f5cb931dd29e35 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Thu, 26 Oct 2023 09:57:59 +0100 Subject: [PATCH 01/33] Update ST V3 impl to (rotating) refresh token impl --- backend/src/config/index.ts | 1 - backend/src/controllers/v1/authController.ts | 5 + .../v3/serviceTokenDataController.ts | 136 ++++++++++++++++-- backend/src/ee/routes/v3/serviceTokenData.ts | 7 +- backend/src/helpers/auth.ts | 63 ++++---- backend/src/middleware/requireAuth.ts | 4 +- backend/src/models/serviceTokenDataV3.ts | 41 +++++- backend/src/routes/v3/secrets.ts | 20 +-- backend/src/validation/serviceTokenDataV3.ts | 14 +- backend/src/variables/authentication.ts | 14 +- .../src/hooks/api/serviceTokens/queries.tsx | 8 +- frontend/src/hooks/api/serviceTokens/types.ts | 14 +- .../AddServiceTokenV3Modal.tsx | 87 ++++++++--- .../ServiceTokenV3Table.tsx | 20 +-- 14 files changed, 337 insertions(+), 97 deletions(-) diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index 402fd5874..c5cb52663 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -24,7 +24,6 @@ export const getJwtRefreshLifetime = async () => (await client.getSecret("JWT_RE export const getJwtServiceSecret = async () => (await client.getSecret("JWT_SERVICE_SECRET")).secretValue; // TODO: deprecate (related to ST V1) export const getJwtSignupLifetime = async () => (await client.getSecret("JWT_SIGNUP_LIFETIME")).secretValue || "15m"; export const getJwtProviderAuthLifetime = async () => (await client.getSecret("JWT_PROVIDER_AUTH_LIFETIME")).secretValue || "15m"; -export const getJwtServiceTokenSecret = async () => (await client.getSecret("JWT_SERVICE_TOKEN_SECRET")).secretValue; export const getMongoURL = async () => (await client.getSecret("MONGO_URL")).secretValue; export const getNodeEnv = async () => (await client.getSecret("NODE_ENV")).secretValue || "production"; export const getVerboseErrorOutput = async () => (await client.getSecret("VERBOSE_ERROR_OUTPUT")).secretValue === "true" && true; diff --git a/backend/src/controllers/v1/authController.ts b/backend/src/controllers/v1/authController.ts index e426eef5a..9e16a4021 100644 --- a/backend/src/controllers/v1/authController.ts +++ b/backend/src/controllers/v1/authController.ts @@ -28,6 +28,11 @@ declare module "jsonwebtoken" { userId: string; refreshVersion?: number; } + export interface ServiceRefreshTokenJwtPayload extends jwt.JwtPayload { + serviceTokenDataId: string; + authTokenType: string; + tokenVersion: number; + } } /** diff --git a/backend/src/ee/controllers/v3/serviceTokenDataController.ts b/backend/src/ee/controllers/v3/serviceTokenDataController.ts index d233cb0d9..d35988140 100644 --- a/backend/src/ee/controllers/v3/serviceTokenDataController.ts +++ b/backend/src/ee/controllers/v3/serviceTokenDataController.ts @@ -1,3 +1,4 @@ +import jwt from "jsonwebtoken"; import { Request, Response } from "express"; import { Types } from "mongoose"; import { @@ -24,10 +25,11 @@ import { getUserProjectPermissions } from "../../services/ProjectRoleService"; import { ForbiddenError } from "@casl/ability"; -import { BadRequestError, ResourceNotFoundError } from "../../../utils/errors"; +import { BadRequestError, ResourceNotFoundError, UnauthorizedRequestError } from "../../../utils/errors"; import { extractIPDetails, isValidIpOrCidr } from "../../../utils/ip"; import { EEAuditLogService, EELicenseService } from "../../services"; -import { getJwtServiceTokenSecret } from "../../../config"; +import { getAuthSecret } from "../../../config"; +import { AuthTokenType } from "../../../variables"; /** * Return project key for service token V3 @@ -56,6 +58,99 @@ export const getServiceTokenDataKey = async (req: Request, res: Response) => { }); } +/** + * Return access and refresh token as per refresh operation + * @param req + * @param res + */ + export const refreshToken = async (req: Request, res: Response) => { + const { + body: { + refresh_token + } + } = await validateRequest(reqValidator.RefreshTokenV3, req); + + const decodedToken = ( + jwt.verify(refresh_token, await getAuthSecret()) + ); + + if (decodedToken.authTokenType !== AuthTokenType.SERVICE_REFRESH_TOKEN) throw UnauthorizedRequestError(); + + let serviceTokenData = await ServiceTokenDataV3.findOne({ + _id: new Types.ObjectId(decodedToken.serviceTokenDataId), + isActive: true + }); + + if (!serviceTokenData) throw UnauthorizedRequestError(); + + if (decodedToken.tokenVersion !== serviceTokenData.tokenVersion) { + // raise alarm + throw UnauthorizedRequestError(); + } + + const response: { + refresh_token?: string; + access_token: string; + expires_in: number; + token_type: string; + } = { + refresh_token, + access_token: "", + expires_in: 0, + token_type: "Bearer" + }; + + if (serviceTokenData.isRefreshTokenRotationEnabled) { + serviceTokenData = await ServiceTokenDataV3.findByIdAndUpdate( + serviceTokenData._id, + { + $inc: { + tokenVersion: 1 + } + }, + { + new: true + } + ); + + if (!serviceTokenData) throw BadRequestError(); + + response.refresh_token = createToken({ + payload: { + serviceTokenDataId: serviceTokenData._id.toString(), + authTokenType: AuthTokenType.SERVICE_REFRESH_TOKEN, + tokenVersion: serviceTokenData.tokenVersion + }, + secret: await getAuthSecret() + }); + } + + response.access_token = createToken({ + payload: { + serviceTokenDataId: serviceTokenData._id.toString(), + authTokenType: AuthTokenType.SERVICE_ACCESS_TOKEN, + tokenVersion: serviceTokenData.tokenVersion + }, + expiresIn: serviceTokenData.accessTokenTTL, + secret: await getAuthSecret() + }); + + response.expires_in = serviceTokenData.accessTokenTTL; + + await ServiceTokenDataV3.findByIdAndUpdate( + serviceTokenData._id, + { + refreshTokenLastUsed: new Date(), + $inc: { refreshTokenUsageCount: 1 } + }, + { + new: true + } + ); + + return res.status(200).send(response); +} + /** * Create service token data V3 * @param req @@ -71,8 +166,10 @@ export const createServiceTokenData = async (req: Request, res: Response) => { scopes, trustedIps, expiresIn, + accessTokenTTL, + isRefreshTokenRotationEnabled, encryptedKey, // for ServiceTokenDataV3Key - nonce // for ServiceTokenDataV3Key + nonce, // for ServiceTokenDataV3Key } } = await validateRequest(reqValidator.CreateServiceTokenV3, req); const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); @@ -112,17 +209,21 @@ export const createServiceTokenData = async (req: Request, res: Response) => { user = req.authData.authPayload._id; } - const isActive = true; + const isActive = false; const serviceTokenData = await new ServiceTokenDataV3({ name, user, workspace: new Types.ObjectId(workspaceId), publicKey, - usageCount: 0, + refreshTokenUsageCount: 0, + accessTokenUsageCount: 0, + tokenVersion: 1, trustedIps: reformattedTrustedIps, scopes, isActive, - expiresAt + expiresAt, + accessTokenTTL, + isRefreshTokenRotationEnabled }).save(); await new ServiceTokenDataV3Key({ @@ -133,18 +234,19 @@ export const createServiceTokenData = async (req: Request, res: Response) => { workspace: new Types.ObjectId(workspaceId) }).save(); - const token = createToken({ + const refreshToken = createToken({ payload: { - _id: serviceTokenData._id.toString() + serviceTokenDataId: serviceTokenData._id.toString(), + authTokenType: AuthTokenType.SERVICE_REFRESH_TOKEN, + tokenVersion: serviceTokenData.tokenVersion }, - expiresIn, - secret: await getJwtServiceTokenSecret() + secret: await getAuthSecret() }); await EEAuditLogService.createAuditLog( req.authData, { - type: EventType.CREATE_SERVICE_TOKEN_V3, + type: EventType.CREATE_SERVICE_TOKEN_V3, // TODO: update metadata: { name, isActive, @@ -160,7 +262,7 @@ export const createServiceTokenData = async (req: Request, res: Response) => { return res.status(200).send({ serviceTokenData, - serviceToken: `stv3.${token}` + refreshToken }); } @@ -178,7 +280,9 @@ export const updateServiceTokenData = async (req: Request, res: Response) => { isActive, scopes, trustedIps, - expiresIn + expiresIn, + accessTokenTTL, + isRefreshTokenRotationEnabled } } = await validateRequest(reqValidator.UpdateServiceTokenV3, req); @@ -233,13 +337,15 @@ export const updateServiceTokenData = async (req: Request, res: Response) => { isActive, scopes, trustedIps: reformattedTrustedIps, - expiresAt + expiresAt, + accessTokenTTL, + isRefreshTokenRotationEnabled }, { new: true } ); - + if (!serviceTokenData) throw BadRequestError({ message: "Failed to update service token" }); diff --git a/backend/src/ee/routes/v3/serviceTokenData.ts b/backend/src/ee/routes/v3/serviceTokenData.ts index 2f421c7ec..ba1d04854 100644 --- a/backend/src/ee/routes/v3/serviceTokenData.ts +++ b/backend/src/ee/routes/v3/serviceTokenData.ts @@ -7,11 +7,16 @@ import { serviceTokenDataController } from "../../controllers/v3"; router.get( "/me/key", requireAuth({ - acceptedAuthModes: [AuthMode.SERVICE_TOKEN_V3] + acceptedAuthModes: [AuthMode.SERVICE_ACCESS_TOKEN] }), serviceTokenDataController.getServiceTokenDataKey ); +router.post( + "/me/token", + serviceTokenDataController.refreshToken +); + router.post( "/", requireAuth({ diff --git a/backend/src/helpers/auth.ts b/backend/src/helpers/auth.ts index a8b13fd82..6cee9b6e0 100644 --- a/backend/src/helpers/auth.ts +++ b/backend/src/helpers/auth.ts @@ -22,8 +22,7 @@ import { import { getAuthSecret, getJwtAuthLifetime, - getJwtRefreshLifetime, - getJwtServiceTokenSecret + getJwtRefreshLifetime } from "../config"; import { AuthMode, @@ -43,7 +42,7 @@ import { getUserAgentType } from "../utils/posthog"; * @param {Object} obj * @param {Object} obj.headers - HTTP request headers object */ -export const validateAuthMode = ({ +export const validateAuthMode = async ({ headers, acceptedAuthModes, }: { @@ -84,13 +83,19 @@ export const validateAuthMode = ({ authMode = AuthMode.SERVICE_TOKEN; authTokenValue = tokenValue; break; - case "stv3": - authMode = AuthMode.SERVICE_TOKEN_V3; - authTokenValue = parts.slice(1).join("."); - break; - default: - authMode = AuthMode.JWT; + default: { + const decodedToken = ( + jwt.verify(tokenValue, await getAuthSecret()) + ); + + if (decodedToken.authTokenType === AuthTokenType.SERVICE_ACCESS_TOKEN) { + authMode = AuthMode.SERVICE_ACCESS_TOKEN; + } else { + authMode = AuthMode.JWT; + } + authTokenValue = tokenValue; + } } } @@ -254,23 +259,17 @@ export const getAuthSTDPayload = async ({ req: Request, authTokenValue: string; }): Promise => { - const decodedToken = ( - jwt.verify(authTokenValue, await getJwtServiceTokenSecret()) + + const decodedToken = ( + jwt.verify(authTokenValue, await getAuthSecret()) ); + + if (decodedToken.authTokenType !== AuthTokenType.SERVICE_ACCESS_TOKEN) throw UnauthorizedRequestError(); - const serviceTokenData = await ServiceTokenDataV3.findOneAndUpdate( - { - _id: new Types.ObjectId(decodedToken._id), - isActive: true - }, - { - lastUsed: new Date(), - $inc: { usageCount: 1 } - }, - { - new: true - } - ); + const serviceTokenData = await ServiceTokenDataV3.findOne({ + _id: new Types.ObjectId(decodedToken.serviceTokenDataId), + isActive: true + }); if (!serviceTokenData) { throw UnauthorizedRequestError({ @@ -288,10 +287,26 @@ export const getAuthSTDPayload = async ({ } ); + throw UnauthorizedRequestError({ + message: "Failed to authenticate", + }); + } else if (decodedToken.tokenVersion !== serviceTokenData.tokenVersion) { + // TODO: raise alarm throw UnauthorizedRequestError({ message: "Failed to authenticate", }); } + + await ServiceTokenDataV3.findByIdAndUpdate( + serviceTokenData._id, + { + accessTokenLastUsed: new Date(), + $inc: { accessTokenUsageCount: 1 } + }, + { + new: true + } + ); return { actor: { diff --git a/backend/src/middleware/requireAuth.ts b/backend/src/middleware/requireAuth.ts index aa74b0dc7..5f3246969 100644 --- a/backend/src/middleware/requireAuth.ts +++ b/backend/src/middleware/requireAuth.ts @@ -35,7 +35,7 @@ const requireAuth = ({ // validate auth token against accepted auth modes [acceptedAuthModes] // and return token type [authTokenType] and value [authTokenValue] - const { authMode, authTokenValue } = validateAuthMode({ + const { authMode, authTokenValue } = await validateAuthMode({ headers: req.headers, acceptedAuthModes, }); @@ -50,7 +50,7 @@ const requireAuth = ({ }); req.serviceTokenData = authData.authPayload; break; - case AuthMode.SERVICE_TOKEN_V3: + case AuthMode.SERVICE_ACCESS_TOKEN: authData = await getAuthSTDV3Payload({ req, authTokenValue diff --git a/backend/src/models/serviceTokenDataV3.ts b/backend/src/models/serviceTokenDataV3.ts index c9895402f..198b60507 100644 --- a/backend/src/models/serviceTokenDataV3.ts +++ b/backend/src/models/serviceTokenDataV3.ts @@ -25,9 +25,14 @@ export interface IServiceTokenDataV3 extends Document { user: Types.ObjectId; publicKey: string; isActive: boolean; - lastUsed?: Date; - usageCount: number; + refreshTokenLastUsed?: Date; + accessTokenLastUsed?: Date; + refreshTokenUsageCount: number; + accessTokenUsageCount: number; + tokenVersion: number; + isRefreshTokenRotationEnabled: boolean; expiresAt?: Date; + accessTokenTTL: number; scopes: Array; trustedIps: Array; } @@ -57,19 +62,43 @@ const serviceTokenDataV3Schema = new Schema( default: true, required: true }, - lastUsed: { + refreshTokenLastUsed: { type: Date, required: false }, - usageCount: { + accessTokenLastUsed: { + type: Date, + required: false + }, + refreshTokenUsageCount: { type: Number, default: 0, required: true }, - expiresAt: { + accessTokenUsageCount: { + type: Number, + default: 0, + required: true + }, + tokenVersion: { + type: Number, + default: 1, + required: true + }, + isRefreshTokenRotationEnabled: { + type: Boolean, + default: false, + required: true + }, + expiresAt: { // consider revising field name type: Date, required: false, - expires: 0 + // expires: 0 + }, + accessTokenTTL: { // seconds + type: Number, + default: 7200, + required: true }, scopes: { type: [ diff --git a/backend/src/routes/v3/secrets.ts b/backend/src/routes/v3/secrets.ts index 960094f87..7aca74e73 100644 --- a/backend/src/routes/v3/secrets.ts +++ b/backend/src/routes/v3/secrets.ts @@ -7,7 +7,7 @@ import { AuthMode } from "../../variables"; router.get( "/raw", requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3] + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_ACCESS_TOKEN] }), secretsController.getSecretsRaw ); @@ -15,7 +15,7 @@ router.get( router.get( "/raw/:secretName", requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3] + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_ACCESS_TOKEN] }), requireBlindIndicesEnabled({ locationWorkspaceId: "query" @@ -29,7 +29,7 @@ router.get( router.post( "/raw/:secretName", requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3] + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_ACCESS_TOKEN] }), requireBlindIndicesEnabled({ locationWorkspaceId: "body" @@ -43,7 +43,7 @@ router.post( router.patch( "/raw/:secretName", requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3] + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_ACCESS_TOKEN] }), requireBlindIndicesEnabled({ locationWorkspaceId: "body" @@ -57,7 +57,7 @@ router.patch( router.delete( "/raw/:secretName", requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3] + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_ACCESS_TOKEN] }), requireBlindIndicesEnabled({ locationWorkspaceId: "body" @@ -71,7 +71,7 @@ router.delete( router.get( "/", requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3] + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_ACCESS_TOKEN] }), requireBlindIndicesEnabled({ locationWorkspaceId: "query" @@ -116,7 +116,7 @@ router.delete( router.post( "/:secretName", requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3] + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_ACCESS_TOKEN] }), requireBlindIndicesEnabled({ locationWorkspaceId: "body" @@ -127,7 +127,7 @@ router.post( router.get( "/:secretName", requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3] + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_ACCESS_TOKEN] }), requireBlindIndicesEnabled({ locationWorkspaceId: "query" @@ -138,7 +138,7 @@ router.get( router.patch( "/:secretName", requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3] + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_ACCESS_TOKEN] }), requireBlindIndicesEnabled({ locationWorkspaceId: "body" @@ -149,7 +149,7 @@ router.patch( router.delete( "/:secretName", requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3] + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_ACCESS_TOKEN] }), requireBlindIndicesEnabled({ locationWorkspaceId: "body" diff --git a/backend/src/validation/serviceTokenDataV3.ts b/backend/src/validation/serviceTokenDataV3.ts index 976f1a1e5..4be60c645 100644 --- a/backend/src/validation/serviceTokenDataV3.ts +++ b/backend/src/validation/serviceTokenDataV3.ts @@ -60,6 +60,12 @@ import { checkIPAgainstBlocklist } from "../utils/ip"; } }; +export const RefreshTokenV3 = z.object({ + body: z.object({ + refresh_token: z.string().trim() + }) +}); + export const CreateServiceTokenV3 = z.object({ body: z.object({ name: z.string().trim(), @@ -80,8 +86,10 @@ export const CreateServiceTokenV3 = z.object({ .array() .min(1), expiresIn: z.number().optional(), + accessTokenTTL: z.number().int().min(1), encryptedKey: z.string().trim(), - nonce: z.string().trim() + nonce: z.string().trim(), + isRefreshTokenRotationEnabled: z.boolean().default(false) }) }); @@ -108,7 +116,9 @@ export const UpdateServiceTokenV3 = z.object({ .array() .min(1) .optional(), - expiresIn: z.number().optional() + expiresIn: z.number().optional(), + accessTokenTTL: z.number().int().min(1).optional(), + isRefreshTokenRotationEnabled: z.boolean().optional() }), }); diff --git a/backend/src/variables/authentication.ts b/backend/src/variables/authentication.ts index 30ba4bf14..2969c88e1 100644 --- a/backend/src/variables/authentication.ts +++ b/backend/src/variables/authentication.ts @@ -1,16 +1,20 @@ +// TODO: merge [AuthTokenType] and [AuthMode] + export enum AuthTokenType { ACCESS_TOKEN = "accessToken", REFRESH_TOKEN = "refreshToken", - SIGNUP_TOKEN = "signupToken", - MFA_TOKEN = "mfaToken", - PROVIDER_TOKEN = "providerToken", - API_KEY = "apiKey" + SIGNUP_TOKEN = "signupToken", // TODO: remove in favor of claim + MFA_TOKEN = "mfaToken", // TODO: remove in favor of claim + PROVIDER_TOKEN = "providerToken", // TODO: remove in favor of claim + API_KEY = "apiKey", + SERVICE_ACCESS_TOKEN = "serviceAccessToken", + SERVICE_REFRESH_TOKEN = "serviceRefreshToken" } export enum AuthMode { JWT = "jwt", SERVICE_TOKEN = "serviceToken", - SERVICE_TOKEN_V3 = "serviceTokenV3", + SERVICE_ACCESS_TOKEN = "serviceAccessToken", API_KEY = "apiKey" } diff --git a/frontend/src/hooks/api/serviceTokens/queries.tsx b/frontend/src/hooks/api/serviceTokens/queries.tsx index 900bb1d34..44786ba0d 100644 --- a/frontend/src/hooks/api/serviceTokens/queries.tsx +++ b/frontend/src/hooks/api/serviceTokens/queries.tsx @@ -88,14 +88,18 @@ export const useUpdateServiceTokenV3 = () => { isActive, scopes, trustedIps, - expiresIn + expiresIn, + accessTokenTTL, + isRefreshTokenRotationEnabled }) => { const { data: { serviceTokenData } } = await apiRequest.patch(`/api/v3/service-token/${serviceTokenDataId}`, { name, isActive, scopes, trustedIps, - expiresIn + expiresIn, + accessTokenTTL, + isRefreshTokenRotationEnabled }); return serviceTokenData; diff --git a/frontend/src/hooks/api/serviceTokens/types.ts b/frontend/src/hooks/api/serviceTokens/types.ts index d47c3bc4c..18e59c68e 100644 --- a/frontend/src/hooks/api/serviceTokens/types.ts +++ b/frontend/src/hooks/api/serviceTokens/types.ts @@ -56,11 +56,15 @@ export type ServiceTokenDataV3 = { name: string; workspace: string; isActive: boolean; - lastUsed?: string; - usageCount: number; + refreshTokenLastUsed?: string; + accessTokenLastUsed?: string; + refreshTokenUsageCount: number; + accessTokenUsageCount: number; scopes: ServiceTokenV3Scope[]; trustedIps: ServiceTokenV3TrustedIp[]; expiresAt?: string; + accessTokenTTL: number; + isRefreshTokenRotationEnabled: boolean; createdAt: string; updatedAt: string; }; @@ -74,12 +78,14 @@ export type CreateServiceTokenDataV3DTO = { ipAddress: string; }[]; expiresIn?: number; + accessTokenTTL: number; encryptedKey: string; nonce: string; + isRefreshTokenRotationEnabled: boolean; } export type CreateServiceTokenDataV3Res = { - serviceToken: string; + refreshToken: string; serviceTokenData: ServiceTokenDataV3; } @@ -92,6 +98,8 @@ export type UpdateServiceTokenDataV3DTO = { ipAddress: string; }[]; expiresIn?: number; + accessTokenTTL?: number; + isRefreshTokenRotationEnabled?: boolean; } export type DeleteServiceTokenDataV3DTO = { diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenV3Section/AddServiceTokenV3Modal.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenV3Section/AddServiceTokenV3Modal.tsx index cc3c81bab..92d701661 100644 --- a/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenV3Section/AddServiceTokenV3Modal.tsx +++ b/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenV3Section/AddServiceTokenV3Modal.tsx @@ -21,6 +21,7 @@ import { ModalContent, Select, SelectItem, + Switch, UpgradePlanModal } from "@app/components/v2"; import { @@ -42,7 +43,7 @@ import { import { UsePopUpState } from "@app/hooks/usePopUp"; const expirations = [ - { label: "Never", value: undefined }, + { label: "Never", value: "" }, { label: "1 day", value: "86400" }, { label: "7 days", value: "604800" }, { label: "1 month", value: "2592000" }, @@ -60,6 +61,13 @@ const permissionsMap: { const schema = yup.object({ name: yup.string().required("ST V3 name is required"), expiresIn: yup.string(), + accessTokenTTL: yup + .string() + .test("is-positive-integer", "Access Token TTL must be a positive integer", (value) => { + const num = parseInt(value, 10); + return !Number.isNaN(num) && num > 0 && String(num) === value; + }) + .required("Access Token TTL is required"), scopes: yup .array( yup.object({ @@ -79,14 +87,15 @@ const schema = yup.object({ .required() .label("Scope"), trustedIps: yup - .array( - yup.object({ - ipAddress: yup.string().max(50).required().label("IP Address") - }) - ) - .min(1) - .required() - .label("Trusted IP") + .array( + yup.object({ + ipAddress: yup.string().max(50).required().label("IP Address") + }) + ) + .min(1) + .required() + .label("Trusted IP"), + isRefreshTokenRotationEnabled: yup.boolean().default(false) }).required(); export type FormData = yup.InferType; @@ -118,6 +127,7 @@ export const AddServiceTokenV3Modal = ({ resolver: yupResolver(schema), defaultValues: { name: "", + accessTokenTTL: "7200", scopes: [{ permission: "read", environment: currentWorkspace?.environments?.[0]?.slug, @@ -135,6 +145,8 @@ export const AddServiceTokenV3Modal = ({ name: string; scopes: ServiceTokenV3Scope[]; trustedIps: ServiceTokenV3TrustedIp[]; + accessTokenTTL: number; + isRefreshTokenRotationEnabled: boolean; }; if (serviceTokenData) { @@ -163,11 +175,14 @@ export const AddServiceTokenV3Modal = ({ return ({ ipAddress: `${ipAddress}${prefix !== undefined ? `/${prefix}` : ""}` }); - }) + }), + accessTokenTTL: String(serviceTokenData.accessTokenTTL), + isRefreshTokenRotationEnabled: serviceTokenData.isRefreshTokenRotationEnabled }); } else { reset({ name: "", + accessTokenTTL: "7200", scopes: [{ permission: "read", environment: currentWorkspace?.environments?.[0]?.slug, @@ -186,8 +201,10 @@ export const AddServiceTokenV3Modal = ({ const onFormSubmit = async ({ name, expiresIn, + accessTokenTTL, scopes, - trustedIps + trustedIps, + isRefreshTokenRotationEnabled }: FormData) => { try { const serviceTokenData = popUp?.serviceTokenV3?.data as { @@ -213,7 +230,9 @@ export const AddServiceTokenV3Modal = ({ name, scopes: reformattedScopes, trustedIps, - expiresIn: expiresIn === "" ? undefined : Number(expiresIn) + expiresIn: expiresIn === "" ? undefined : Number(expiresIn), + accessTokenTTL: Number(accessTokenTTL), + isRefreshTokenRotationEnabled }); } else { // create @@ -239,21 +258,23 @@ export const AddServiceTokenV3Modal = ({ privateKey: localStorage.getItem("PRIVATE_KEY") as string }); - const { serviceToken } = await createMutateAsync({ + const { refreshToken } = await createMutateAsync({ name, workspaceId: currentWorkspace._id, publicKey, scopes: reformattedScopes, trustedIps, expiresIn: expiresIn === "" ? undefined : Number(expiresIn), + accessTokenTTL: Number(accessTokenTTL), encryptedKey: ciphertext, - nonce + nonce, + isRefreshTokenRotationEnabled }); const downloadData = { publicKey, privateKey, - serviceToken + refreshToken }; const blob = new Blob([JSON.stringify(downloadData, null, 2)], { type: "application/json" }); @@ -476,10 +497,10 @@ export const AddServiceTokenV3Modal = ({ ( )} /> + ( + + + + )} + /> +
+ ( + onChange(isChecked)} + isChecked={value} + > + Refresh Token Rotation + + )} + /> +
+ +
+ } + > + {t("integrations.why-infisical-needs-access")} + + + handlePopUpToggle("deleteRotation", isOpen)} + deleteKey="confirm" + onDeleteApproved={handleDeleteRotation} + /> + handlePopUpToggle("upgradePlan", isOpen)} + text="You can add secret rotation if you switch to Infisical's Team plan." + /> + + ); + }, + { action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.SecretRotation } +); diff --git a/frontend/src/views/SecretRotationPage/SecretRotationPage.utils.ts b/frontend/src/views/SecretRotationPage/SecretRotationPage.utils.ts new file mode 100644 index 000000000..6dd346f8e --- /dev/null +++ b/frontend/src/views/SecretRotationPage/SecretRotationPage.utils.ts @@ -0,0 +1,30 @@ +import { UserWsKeyPair } from "@app/hooks/api/types"; + +import { + decryptAssymmetric, + encryptAssymmetric +} from "../../components/utilities/cryptography/crypto"; + +// refactor these to common function in frontend +export const generateBotKey = (botPublicKey: string, latestKey: UserWsKeyPair) => { + const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY"); + + if (!PRIVATE_KEY) { + throw new Error("Private Key missing"); + } + + const WORKSPACE_KEY = decryptAssymmetric({ + ciphertext: latestKey.encryptedKey, + nonce: latestKey.nonce, + publicKey: latestKey.sender.publicKey, + privateKey: PRIVATE_KEY + }); + + const { ciphertext, nonce } = encryptAssymmetric({ + plaintext: WORKSPACE_KEY, + publicKey: botPublicKey, + privateKey: PRIVATE_KEY + }); + + return { encryptedKey: ciphertext, nonce }; +}; diff --git a/frontend/src/views/SecretRotationPage/components/CreateRotationForm/CreateRotationForm.tsx b/frontend/src/views/SecretRotationPage/components/CreateRotationForm/CreateRotationForm.tsx new file mode 100644 index 000000000..c5493159e --- /dev/null +++ b/frontend/src/views/SecretRotationPage/components/CreateRotationForm/CreateRotationForm.tsx @@ -0,0 +1,149 @@ +import { useRef, useState } from "react"; +import { AnimatePresence, motion } from "framer-motion"; + +import { Modal, ModalContent, Step, Stepper } from "@app/components/v2"; +import { useCreateSecretRotation } from "@app/hooks/api"; +import { TSecretRotationProvider } from "@app/hooks/api/types"; + +import { useNotificationContext } from "~/components/context/Notifications/NotificationProvider"; + +import { GeneralDetailsForm, TFormSchema as TGeneralFormSchema } from "./steps/GeneralDetailsForm"; +import { RotationInputForm } from "./steps/RotationInputForm"; +import { + RotationOutputForm, + TFormSchema as TRotationOutputSchema +} from "./steps/RotationOutputForm"; + +const WIZARD_STEPS = [ + { + title: "General" + }, + { + title: "Inputs" + }, + { + title: "Secret Mapping" + } +]; + +type Props = { + isOpen?: boolean; + onToggle: (isOpen: boolean) => void; + customProvider?: string; + workspaceId: string; + provider: TSecretRotationProvider; +}; + +export const CreateRotationForm = ({ + isOpen, + onToggle, + provider, + workspaceId, + customProvider +}: Props) => { + const [wizardStep, setWizardStep] = useState(0); + const wizardData = useRef<{ + general?: TGeneralFormSchema; + input?: Record; + output?: TRotationOutputSchema; + }>({}); + const { createNotification } = useNotificationContext(); + + const { mutateAsync: createSecretRotation } = useCreateSecretRotation(); + + const handleFormCancel = () => { + onToggle(false); + setWizardStep(0); + wizardData.current = {}; + }; + + const handleFormSubmit = async () => { + if (!wizardData.current.general || !wizardData.current.input || !wizardData.current.output) + return; + try { + await createSecretRotation({ + workspaceId, + provider: provider.name, + customProvider, + secretPath: wizardData.current.general.secretPath, + environment: wizardData.current.general.environment, + interval: wizardData.current.general.interval, + inputs: wizardData.current.input, + outputs: wizardData.current.output + }); + setWizardStep(0); + onToggle(false); + wizardData.current = {}; + } catch (error) { + console.log(error); + createNotification({ + type: "error", + text: "Failed to create secret rotation" + }); + } + }; + + return ( + { + onToggle(state); + setWizardStep(0); + wizardData.current = {}; + }} + > + + + {WIZARD_STEPS.map(({ title }, index) => ( + + ))} + + + {wizardStep === 0 && ( + + { + wizardData.current.general = data; + setWizardStep((state) => state + 1); + }} + /> + + )} + {wizardStep === 1 && ( + { + wizardData.current.input = data; + setWizardStep((state) => state + 1); + }} + inputSchema={provider.template?.inputs || {}} + /> + )} + {wizardStep === 2 && ( + { + wizardData.current.output = data; + await handleFormSubmit(); + }} + /> + )} + + + + ); +}; diff --git a/frontend/src/views/SecretRotationPage/components/CreateRotationForm/index.tsx b/frontend/src/views/SecretRotationPage/components/CreateRotationForm/index.tsx new file mode 100644 index 000000000..8392c4cdb --- /dev/null +++ b/frontend/src/views/SecretRotationPage/components/CreateRotationForm/index.tsx @@ -0,0 +1 @@ +export { CreateRotationForm } from "./CreateRotationForm"; diff --git a/frontend/src/views/SecretRotationPage/components/CreateRotationForm/steps/GeneralDetailsForm.tsx b/frontend/src/views/SecretRotationPage/components/CreateRotationForm/steps/GeneralDetailsForm.tsx new file mode 100644 index 000000000..b83b19974 --- /dev/null +++ b/frontend/src/views/SecretRotationPage/components/CreateRotationForm/steps/GeneralDetailsForm.tsx @@ -0,0 +1,93 @@ +import { Controller, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { Button, FormControl, Input, Select, SelectItem } from "@app/components/v2"; +import { useWorkspace } from "@app/context"; + +const formSchema = z.object({ + environment: z.string().trim(), + secretPath: z.string().trim().default("/"), + interval: z.number() +}); + +export type TFormSchema = z.infer; +type Props = { + onSubmit: (data: TFormSchema) => void; + onCancel: () => void; +}; + +export const GeneralDetailsForm = ({ onSubmit, onCancel }: Props) => { + const { currentWorkspace } = useWorkspace(); + const environments = currentWorkspace?.environments || []; + const { + control, + handleSubmit, + formState: { isSubmitting } + } = useForm({ + resolver: zodResolver(formSchema) + }); + + return ( +
+ ( + + + + )} + /> + ( + + + + )} + /> + ( + + field.onChange(parseInt(evt.target.value, 10))} + /> + + )} + /> +
+ + +
+ + ); +}; diff --git a/frontend/src/views/SecretRotationPage/components/CreateRotationForm/steps/RotationInputForm.tsx b/frontend/src/views/SecretRotationPage/components/CreateRotationForm/steps/RotationInputForm.tsx new file mode 100644 index 000000000..baacfc2a5 --- /dev/null +++ b/frontend/src/views/SecretRotationPage/components/CreateRotationForm/steps/RotationInputForm.tsx @@ -0,0 +1,61 @@ +import { Controller, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { Button, FormControl, SecretInput } from "@app/components/v2"; + +type Props = { + onSubmit: (data: Record) => void; + onCancel: () => void; + inputSchema: { + properties: Record; + required: string[]; + }; +}; + +const formSchema = z.record(z.string().trim().optional()); + +export const RotationInputForm = ({ onSubmit, onCancel, inputSchema }: Props) => { + const { + control, + handleSubmit, + formState: { isSubmitting } + } = useForm>({ + resolver: zodResolver(formSchema) + }); + + return ( +
+ {Object.keys(inputSchema.properties || {}).map((inputName) => ( + ( + + + + )} + /> + ))} +
+ + +
+ + ); +}; diff --git a/frontend/src/views/SecretRotationPage/components/CreateRotationForm/steps/RotationOutputForm.tsx b/frontend/src/views/SecretRotationPage/components/CreateRotationForm/steps/RotationOutputForm.tsx new file mode 100644 index 000000000..ed60ba912 --- /dev/null +++ b/frontend/src/views/SecretRotationPage/components/CreateRotationForm/steps/RotationOutputForm.tsx @@ -0,0 +1,80 @@ +import { Controller, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { Button, FormControl, Select, SelectItem } from "@app/components/v2"; +import { useWorkspace } from "@app/context"; +import { useGetProjectSecrets, useGetUserWsKey } from "@app/hooks/api"; + +const formSchema = z.record(z.string()); + +export type TFormSchema = z.infer; +type Props = { + environment: string; + secretPath: string; + outputSchema: Record; + onSubmit: (data: TFormSchema) => void; + onCancel: () => void; +}; + +export const RotationOutputForm = ({ + onSubmit, + onCancel, + environment, + secretPath, + outputSchema = {} +}: Props) => { + const { currentWorkspace } = useWorkspace(); + const workspaceId = currentWorkspace?._id || ""; + const { + control, + handleSubmit, + formState: { isSubmitting } + } = useForm({ + resolver: zodResolver(formSchema) + }); + + const { data: userWsKey } = useGetUserWsKey(workspaceId); + const { data: secrets } = useGetProjectSecrets({ + workspaceId, + environment, + secretPath, + decryptFileKey: userWsKey! + }); + + return ( +
+ {Object.keys(outputSchema).map((outputName) => ( + ( + + + + )} + /> + ))} +
+ + +
+ + ); +}; diff --git a/frontend/src/views/SecretRotationPage/index.tsx b/frontend/src/views/SecretRotationPage/index.tsx new file mode 100644 index 000000000..c2f41e76e --- /dev/null +++ b/frontend/src/views/SecretRotationPage/index.tsx @@ -0,0 +1 @@ +export { SecretRotationPage } from "./SecretRotationPage"; From c9c40521b258c6f5c624aa23601e957b6b4b7f32 Mon Sep 17 00:00:00 2001 From: Akhil Mohan Date: Thu, 26 Oct 2023 14:12:39 +0530 Subject: [PATCH 14/33] feat(secret-rotation): added db ssl support --- .../src/controllers/v3/secretsController.ts | 20 +- .../ee/secretRotation/providerTemplates.ts | 215 ------------------ backend/src/ee/secretRotation/queue.ts | 12 +- backend/src/ee/secretRotation/service.ts | 16 +- .../src/ee/secretRotation/templates/index.ts | 28 +++ .../src/ee/secretRotation/templates/mysql.ts | 75 ++++++ .../ee/secretRotation/templates/postgres.ts | 74 ++++++ .../ee/secretRotation/templates/sendgrid.ts | 59 +++++ backend/src/helpers/secrets.ts | 14 +- .../services/SecretService/index.ts | 1 - backend/src/validation/secrets.ts | 2 - frontend/public/lotties/rotation.json | 1 + .../src/components/v2/Stepper/Stepper.tsx | 4 +- frontend/src/layouts/AppLayout/AppLayout.tsx | 2 +- .../SecretRotationPage/SecretRotationPage.tsx | 8 +- .../CreateRotationForm/CreateRotationForm.tsx | 73 +++--- .../steps/GeneralDetailsForm.tsx | 93 -------- .../steps/RotationInputForm.tsx | 1 - .../steps/RotationOutputForm.tsx | 101 ++++++-- 19 files changed, 387 insertions(+), 412 deletions(-) delete mode 100644 backend/src/ee/secretRotation/providerTemplates.ts create mode 100644 backend/src/ee/secretRotation/templates/index.ts create mode 100644 backend/src/ee/secretRotation/templates/mysql.ts create mode 100644 backend/src/ee/secretRotation/templates/postgres.ts create mode 100644 backend/src/ee/secretRotation/templates/sendgrid.ts create mode 100644 frontend/public/lotties/rotation.json delete mode 100644 frontend/src/views/SecretRotationPage/components/CreateRotationForm/steps/GeneralDetailsForm.tsx diff --git a/backend/src/controllers/v3/secretsController.ts b/backend/src/controllers/v3/secretsController.ts index 2e0c8514a..31b645432 100644 --- a/backend/src/controllers/v3/secretsController.ts +++ b/backend/src/controllers/v3/secretsController.ts @@ -140,7 +140,7 @@ export const getSecretsRaw = async (req: Request, res: Response) => { query: { secretPath, environment, workspaceId } } = validatedData; const { - query: { folderId, include_imports: includeImports } + query: { include_imports: includeImports } } = validatedData; // if the service token has single scope, it will get all secrets for that scope by default @@ -156,13 +156,6 @@ export const getSecretsRaw = async (req: Request, res: Response) => { workspaceId = serviceTokenDetails.workspace.toString(); } - if (folderId && folderId !== "root") { - const folder = await Folder.findOne({ workspace: workspaceId, environment }); - if (!folder) throw BadRequestError({ message: "Folder not found" }); - - secretPath = getFolderWithPathFromId(folder.nodes, folderId).folderPath; - } - if (!environment || !workspaceId) throw BadRequestError({ message: "Missing environment or workspace id" }); @@ -177,7 +170,6 @@ export const getSecretsRaw = async (req: Request, res: Response) => { const secrets = await SecretService.getSecrets({ workspaceId: new Types.ObjectId(workspaceId), environment, - folderId, secretPath, authData: req.authData }); @@ -467,20 +459,13 @@ export const deleteSecretByNameRaw = async (req: Request, res: Response) => { export const getSecrets = async (req: Request, res: Response) => { const validatedData = await validateRequest(reqValidator.GetSecretsV3, req); const { - query: { environment, workspaceId, include_imports: includeImports, folderId } + query: { environment, workspaceId, include_imports: includeImports } } = validatedData; let { query: { secretPath } } = validatedData; - if (folderId && folderId !== "root") { - const folder = await Folder.findOne({ workspace: workspaceId, environment }); - if (!folder) return res.send({ secrets: [] }); - - secretPath = getFolderWithPathFromId(folder.nodes, folderId).folderPath; - } - const { authVerifier: permissionCheckFn } = await checkSecretsPermission({ authData: req.authData, workspaceId, @@ -492,7 +477,6 @@ export const getSecrets = async (req: Request, res: Response) => { const secrets = await SecretService.getSecrets({ workspaceId: new Types.ObjectId(workspaceId), environment, - folderId, secretPath, authData: req.authData }); diff --git a/backend/src/ee/secretRotation/providerTemplates.ts b/backend/src/ee/secretRotation/providerTemplates.ts deleted file mode 100644 index 4becc2ccd..000000000 --- a/backend/src/ee/secretRotation/providerTemplates.ts +++ /dev/null @@ -1,215 +0,0 @@ -import { - ISecretRotationProviderTemplate, - TProviderFunctionTypes, - TDbProviderClients, - TAssignOp -} from "./types"; - -const SENDGRID_TEMPLATE = { - inputs: { - type: "object" as const, - properties: { - admin_api_key: { type: "string" as const }, - scopes: { type: "array", items: { type: "string" as const } } - }, - required: ["admin_api_key", "scopes"], - additionalProperties: false - }, - outputs: { - api_key: { type: "string" } - }, - internal: { - api_key_id: { type: "string" } - }, - functions: { - set: { - type: TProviderFunctionTypes.HTTP as const, - url: "https://api.sendgrid.com/v3/api_keys", - method: "POST", - header: { - Authorization: "Bearer ${inputs.admin_api_key}" - }, - body: { - name: "infisical-${random | 16}", - scopes: { ref: "inputs.scopes" } - }, - setter: { - "outputs.api_key": { - assign: TAssignOp.JmesPath as const, - path: "api_key" - }, - "internal.api_key_id": { - assign: TAssignOp.JmesPath as const, - path: "api_key_id" - } - } - }, - remove: { - type: TProviderFunctionTypes.HTTP as const, - url: "https://api.sendgrid.com/v3/api_keys/${internal.api_key_id}", - header: { - Authorization: "Bearer ${inputs.admin_api_key}" - }, - method: "DELETE" - }, - test: { - type: TProviderFunctionTypes.HTTP as const, - url: "https://api.sendgrid.com/v3/api_keys/${internal.api_key_id}", - header: { - Authorization: "Bearer ${inputs.admin_api_key}" - }, - method: "GET" - } - } -}; - -const POSTGRES_TEMPLATE = { - inputs: { - type: "object" as const, - properties: { - admin_username: { type: "string" as const }, - admin_password: { type: "string" as const }, - host: { type: "string" as const }, - database: { type: "string" as const }, - port: { type: "integer" as const, default: "5432" }, - username1: { type: "string", default: "infisical-pg-user1" }, - username2: { type: "string", default: "infisical-pg-user2" } - }, - required: ["admin_username", "admin_password", "host", "database"], - additionalProperties: false - }, - outputs: { - db_username: { type: "string" }, - db_password: { type: "string" } - }, - internal: { - rotated_password: { type: "string" }, - username: { type: "string" } - }, - functions: { - set: { - type: TProviderFunctionTypes.DB as const, - client: TDbProviderClients.Pg, - username: "${inputs.admin_username}", - password: "${inputs.admin_password}", - host: "${inputs.host}", - database: "${inputs.database}", - port: "${inputs.port}", - query: "ALTER USER ${internal.username} WITH PASSWORD '${internal.rotated_password}'", - setter: { - "outputs.db_username": { - assign: TAssignOp.Direct as const, - value: "${internal.username}" - }, - "outputs.db_password": { - assign: TAssignOp.Direct as const, - value: "${internal.rotated_password}" - } - }, - pre: { - "internal.rotated_password": { - assign: TAssignOp.Direct as const, - value: "${random | 32}" - } - } - }, - test: { - type: TProviderFunctionTypes.DB as const, - client: TDbProviderClients.Pg, - username: "${internal.username}", - password: "${internal.rotated_password}", - host: "${inputs.host}", - database: "${inputs.database}", - port: "${inputs.port}", - query: "SELECT NOW()" - } - } -}; - -const MYSQL_TEMPLATE = { - inputs: { - type: "object" as const, - properties: { - admin_username: { type: "string" as const }, - admin_password: { type: "string" as const }, - host: { type: "string" as const }, - database: { type: "string" as const }, - port: { type: "integer" as const, default: "3306" }, - username1: { type: "string", default: "infisical-sql-user1" }, - username2: { type: "string", default: "infisical-sql-user2" } - }, - required: ["admin_username", "admin_password", "host", "database"], - additionalProperties: false - }, - outputs: { - db_username: { type: "string" }, - db_password: { type: "string" } - }, - internal: { - rotated_password: { type: "string" }, - username: { type: "string" } - }, - functions: { - set: { - type: TProviderFunctionTypes.DB as const, - client: TDbProviderClients.Sql, - username: "${inputs.admin_username}", - password: "${inputs.admin_password}", - host: "${inputs.host}", - database: "${inputs.database}", - port: "${inputs.port}", - query: - "ALTER USER ${internal.username} IDENTIFIED WITH mysql_native_password BY '${internal.rotated_password}'", - setter: { - "outputs.db_username": { - assign: TAssignOp.Direct as const, - value: "${internal.username}" - }, - "outputs.db_password": { - assign: TAssignOp.Direct as const, - value: "${internal.rotated_password}" - } - }, - pre: { - "internal.rotated_password": { - assign: TAssignOp.Direct as const, - value: "${random | 32}" - } - } - }, - test: { - type: TProviderFunctionTypes.DB as const, - client: TDbProviderClients.Sql, - username: "${internal.username}", - password: "${internal.rotated_password}", - host: "${inputs.host}", - database: "${inputs.database}", - port: "${inputs.port}", - query: "SELECT NOW()" - } - } -}; - -export const providerRotationTemplates: ISecretRotationProviderTemplate[] = [ - { - name: "sendgrid", - title: "Twilio Sendgrid", - image: "sendgrid.png", - description: "Rotate Twilio Sendgrid API keys", - template: SENDGRID_TEMPLATE - }, - { - name: "postgres", - title: "PostgreSQL", - image: "postgres.png", - description: "Rotate PostgreSQL/CockroachDB user credentials", - template: POSTGRES_TEMPLATE - }, - { - name: "mysql", - title: "MySQL", - image: "mysql.png", - description: "Rotate MySQL@7/MariaDB user credentials", - template: MYSQL_TEMPLATE - } -]; diff --git a/backend/src/ee/secretRotation/queue.ts b/backend/src/ee/secretRotation/queue.ts index a5fb368ec..e85cb88ec 100644 --- a/backend/src/ee/secretRotation/queue.ts +++ b/backend/src/ee/secretRotation/queue.ts @@ -8,7 +8,7 @@ import mysql from "mysql"; import { client, getRootEncryptionKey } from "../../config"; import { BotService, TelemetryService } from "../../services"; import { SecretRotation } from "./models"; -import { providerRotationTemplates } from "./providerTemplates"; +import { rotationTemplates } from "./templates"; import { ISecretRotationData, ISecretRotationEncData, @@ -88,13 +88,14 @@ const secretRotationHttpFn = async ( const secretRotationDbFn = async (func: TDbProviderFunction, variables: ISecretRotationData) => { const { type, client, pre, ...dbConnection } = func; - const { username, password, host, database, port, query } = interpolate( + const { username, password, host, database, port, query, ca } = interpolate( dbConnection, getInterpolationValue(variables) ); + const ssl = ca ? { rejectUnauthorized: false, ca } : undefined; if (host === "localhost" || host === "127.0.0.1") throw new Error("Invalid db host"); if (client === TDbProviderClients.Pg) { - const pgClient = new PgClient({ user: username, password, host, database, port }); + const pgClient = new PgClient({ user: username, password, host, database, port, ssl }); await pgClient.connect(); const res = await pgClient.query(query); await pgClient.end(); @@ -106,7 +107,8 @@ const secretRotationDbFn = async (func: TDbProviderFunction, variables: ISecretR host, database, port, - connectionLimit: 1 + connectionLimit: 1, + ssl }); const res = await new Promise((resolve, reject) => { sqlClient.query(query, (err, data) => { @@ -193,7 +195,7 @@ secretRotationQueue.process(async (job: Job) => { ]; }>("outputs.secret"); - const infisicalRotationProvider = providerRotationTemplates.find( + const infisicalRotationProvider = rotationTemplates.find( ({ name }) => name === secretRotation?.provider ); diff --git a/backend/src/ee/secretRotation/service.ts b/backend/src/ee/secretRotation/service.ts index 7e02dc860..8476dcbb8 100644 --- a/backend/src/ee/secretRotation/service.ts +++ b/backend/src/ee/secretRotation/service.ts @@ -1,13 +1,5 @@ -import { - ISecretRotationEncData, - ISecretRotationProviderTemplate, - TCreateSecretRotation, - TGetProviderTemplates -} from "./types"; -import { - providerRotationTemplates as infisicalRotationTemplates, - providerRotationTemplates -} from "./providerTemplates"; +import { ISecretRotationEncData, TCreateSecretRotation, TGetProviderTemplates } from "./types"; +import { rotationTemplates } from "./templates"; import { SecretRotation } from "./models"; import { client, getRootEncryptionKey } from "../../config"; import { BadRequestError } from "../../utils/errors"; @@ -19,7 +11,7 @@ const ajv = new Ajv(); export const getProviderTemplate = async ({ workspaceId }: TGetProviderTemplates) => { return { custom: [], - providers: infisicalRotationTemplates + providers: rotationTemplates }; }; @@ -32,7 +24,7 @@ export const createSecretRotation = async ({ inputs, outputs }: TCreateSecretRotation) => { - const rotationTemplate = providerRotationTemplates.find(({ name }) => name === provider); + const rotationTemplate = rotationTemplates.find(({ name }) => name === provider); if (!rotationTemplate) throw BadRequestError({ message: "Provider not found" }); const formattedInputs: Record = {}; diff --git a/backend/src/ee/secretRotation/templates/index.ts b/backend/src/ee/secretRotation/templates/index.ts new file mode 100644 index 000000000..063d5149f --- /dev/null +++ b/backend/src/ee/secretRotation/templates/index.ts @@ -0,0 +1,28 @@ +import { ISecretRotationProviderTemplate } from "../types"; +import { MYSQL_TEMPLATE } from "./mysql"; +import { POSTGRES_TEMPLATE } from "./postgres"; +import { SENDGRID_TEMPLATE } from "./sendgrid"; + +export const rotationTemplates: ISecretRotationProviderTemplate[] = [ + { + name: "sendgrid", + title: "Twilio Sendgrid", + image: "sendgrid.png", + description: "Rotate Twilio Sendgrid API keys", + template: SENDGRID_TEMPLATE + }, + { + name: "postgres", + title: "PostgreSQL", + image: "postgres.png", + description: "Rotate PostgreSQL/CockroachDB user credentials", + template: POSTGRES_TEMPLATE + }, + { + name: "mysql", + title: "MySQL", + image: "mysql.png", + description: "Rotate MySQL@7/MariaDB user credentials", + template: MYSQL_TEMPLATE + } +]; diff --git a/backend/src/ee/secretRotation/templates/mysql.ts b/backend/src/ee/secretRotation/templates/mysql.ts new file mode 100644 index 000000000..fa730b881 --- /dev/null +++ b/backend/src/ee/secretRotation/templates/mysql.ts @@ -0,0 +1,75 @@ +import { TProviderFunctionTypes, TDbProviderClients, TAssignOp } from "../types"; + +export const MYSQL_TEMPLATE = { + inputs: { + type: "object" as const, + properties: { + admin_username: { type: "string" as const }, + admin_password: { type: "string" as const }, + host: { type: "string" as const }, + database: { type: "string" as const }, + port: { type: "integer" as const, default: "3306" }, + username1: { type: "string", default: "infisical-sql-user1" }, + username2: { type: "string", default: "infisical-sql-user2" }, + ca: { type: "string" } + }, + required: [ + "admin_username", + "admin_password", + "host", + "database", + "username1", + "username2", + "port" + ], + additionalProperties: false + }, + outputs: { + db_username: { type: "string" }, + db_password: { type: "string" } + }, + internal: { + rotated_password: { type: "string" }, + username: { type: "string" } + }, + functions: { + set: { + type: TProviderFunctionTypes.DB as const, + client: TDbProviderClients.Sql, + username: "${inputs.admin_username}", + password: "${inputs.admin_password}", + host: "${inputs.host}", + database: "${inputs.database}", + port: "${inputs.port}", + ca: "${inputs.ca}", + query: + "ALTER USER ${internal.username} IDENTIFIED WITH mysql_native_password BY '${internal.rotated_password}'", + setter: { + "outputs.db_username": { + assign: TAssignOp.Direct as const, + value: "${internal.username}" + }, + "outputs.db_password": { + assign: TAssignOp.Direct as const, + value: "${internal.rotated_password}" + } + }, + pre: { + "internal.rotated_password": { + assign: TAssignOp.Direct as const, + value: "${random | 32}" + } + } + }, + test: { + type: TProviderFunctionTypes.DB as const, + client: TDbProviderClients.Sql, + username: "${internal.username}", + password: "${internal.rotated_password}", + host: "${inputs.host}", + database: "${inputs.database}", + port: "${inputs.port}", + query: "SELECT NOW()" + } + } +}; diff --git a/backend/src/ee/secretRotation/templates/postgres.ts b/backend/src/ee/secretRotation/templates/postgres.ts new file mode 100644 index 000000000..ca489570b --- /dev/null +++ b/backend/src/ee/secretRotation/templates/postgres.ts @@ -0,0 +1,74 @@ +import { TProviderFunctionTypes, TDbProviderClients, TAssignOp } from "../types"; + +export const POSTGRES_TEMPLATE = { + inputs: { + type: "object" as const, + properties: { + admin_username: { type: "string" as const }, + admin_password: { type: "string" as const }, + host: { type: "string" as const }, + database: { type: "string" as const }, + port: { type: "integer" as const, default: "5432" }, + username1: { type: "string", default: "infisical-pg-user1" }, + username2: { type: "string", default: "infisical-pg-user2" }, + ca: { type: "string" } + }, + required: [ + "admin_username", + "admin_password", + "host", + "database", + "username1", + "username2", + "port" + ], + additionalProperties: false + }, + outputs: { + db_username: { type: "string" }, + db_password: { type: "string" } + }, + internal: { + rotated_password: { type: "string" }, + username: { type: "string" } + }, + functions: { + set: { + type: TProviderFunctionTypes.DB as const, + client: TDbProviderClients.Pg, + username: "${inputs.admin_username}", + password: "${inputs.admin_password}", + host: "${inputs.host}", + database: "${inputs.database}", + port: "${inputs.port}", + ca: "${inputs.ca}", + query: "ALTER USER ${internal.username} WITH PASSWORD '${internal.rotated_password}'", + setter: { + "outputs.db_username": { + assign: TAssignOp.Direct as const, + value: "${internal.username}" + }, + "outputs.db_password": { + assign: TAssignOp.Direct as const, + value: "${internal.rotated_password}" + } + }, + pre: { + "internal.rotated_password": { + assign: TAssignOp.Direct as const, + value: "${random | 32}" + } + } + }, + test: { + type: TProviderFunctionTypes.DB as const, + client: TDbProviderClients.Pg, + username: "${internal.username}", + password: "${internal.rotated_password}", + host: "${inputs.host}", + database: "${inputs.database}", + port: "${inputs.port}", + query: "SELECT NOW()" + } + } +}; diff --git a/backend/src/ee/secretRotation/templates/sendgrid.ts b/backend/src/ee/secretRotation/templates/sendgrid.ts new file mode 100644 index 000000000..454c9a145 --- /dev/null +++ b/backend/src/ee/secretRotation/templates/sendgrid.ts @@ -0,0 +1,59 @@ +import { TProviderFunctionTypes, TAssignOp } from "../types"; + +export const SENDGRID_TEMPLATE = { + inputs: { + type: "object" as const, + properties: { + admin_api_key: { type: "string" as const }, + scopes: { type: "array", items: { type: "string" as const } } + }, + required: ["admin_api_key", "scopes"], + additionalProperties: false + }, + outputs: { + api_key: { type: "string" } + }, + internal: { + api_key_id: { type: "string" } + }, + functions: { + set: { + type: TProviderFunctionTypes.HTTP as const, + url: "https://api.sendgrid.com/v3/api_keys", + method: "POST", + header: { + Authorization: "Bearer ${inputs.admin_api_key}" + }, + body: { + name: "infisical-${random | 16}", + scopes: { ref: "inputs.scopes" } + }, + setter: { + "outputs.api_key": { + assign: TAssignOp.JmesPath as const, + path: "api_key" + }, + "internal.api_key_id": { + assign: TAssignOp.JmesPath as const, + path: "api_key_id" + } + } + }, + remove: { + type: TProviderFunctionTypes.HTTP as const, + url: "https://api.sendgrid.com/v3/api_keys/${internal.api_key_id}", + header: { + Authorization: "Bearer ${inputs.admin_api_key}" + }, + method: "DELETE" + }, + test: { + type: TProviderFunctionTypes.HTTP as const, + url: "https://api.sendgrid.com/v3/api_keys/${internal.api_key_id}", + header: { + Authorization: "Bearer ${inputs.admin_api_key}" + }, + method: "GET" + } + } +}; diff --git a/backend/src/helpers/secrets.ts b/backend/src/helpers/secrets.ts index 82a98d1e9..4b38079c1 100644 --- a/backend/src/helpers/secrets.ts +++ b/backend/src/helpers/secrets.ts @@ -553,14 +553,22 @@ export const getSecretsHelper = async ({ workspaceId, environment, authData, - folderId, secretPath = "/" }: GetSecretsParams) => { let secrets: ISecret[] = []; // if using service token filter towards the folderId by secretpath - if (!folderId) { - folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath); + const folders = await Folder.findOne({ + workspace: workspaceId, + environment + }); + let folderId = "root"; + if (!folders && folderId !== "root") return []; + // get folder from folder tree + if (folders) { + const folder = getFolderByPath(folders.nodes, secretPath); + if (!folder) return []; + folderId = folder?.id; } // get personal secrets first diff --git a/backend/src/interfaces/services/SecretService/index.ts b/backend/src/interfaces/services/SecretService/index.ts index a2d9c5bb5..631114a79 100644 --- a/backend/src/interfaces/services/SecretService/index.ts +++ b/backend/src/interfaces/services/SecretService/index.ts @@ -26,7 +26,6 @@ export interface CreateSecretParams { export interface GetSecretsParams { workspaceId: Types.ObjectId; environment: string; - folderId?: string; secretPath: string; authData: AuthData; } diff --git a/backend/src/validation/secrets.ts b/backend/src/validation/secrets.ts index 174ffa791..799e9531d 100644 --- a/backend/src/validation/secrets.ts +++ b/backend/src/validation/secrets.ts @@ -228,7 +228,6 @@ export const GetSecretsRawV3 = z.object({ workspaceId: z.string().trim().optional(), environment: z.string().trim().optional(), secretPath: z.string().trim().default("/"), - folderId: z.string().trim().optional(), include_imports: z .enum(["true", "false"]) .default("false") @@ -302,7 +301,6 @@ export const GetSecretsV3 = z.object({ workspaceId: z.string().trim(), environment: z.string().trim(), secretPath: z.string().trim().default("/"), - folderId: z.string().trim().optional(), include_imports: z .enum(["true", "false"]) .default("false") diff --git a/frontend/public/lotties/rotation.json b/frontend/public/lotties/rotation.json new file mode 100644 index 000000000..5ea9f1551 --- /dev/null +++ b/frontend/public/lotties/rotation.json @@ -0,0 +1 @@ +{"v":"5.8.1","fr":60,"ip":130,"op":250,"w":430,"h":430,"nm":"232-arrow-21","ddd":0,"assets":[{"id":"comp_0","nm":"in-reveal","fr":60,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Vector 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-45,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.724,"y":0.959},"o":{"x":0.249,"y":0.15},"t":0,"s":[254.591,233.94,0],"to":[91.728,-89.288,0],"ti":[15.825,17.58,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.212,"y":0.03},"t":20,"s":[371.54,52.516,0],"to":[-13.998,-15.55,0],"ti":[108.509,-104.074,0]},{"i":{"x":0.777,"y":0.968},"o":{"x":0.167,"y":0.166},"t":38.854,"s":[190.63,169.978,0],"to":[-116.05,111.307,0],"ti":[-17.451,-15.403,0]},{"i":{"x":0.34,"y":1},"o":{"x":0.092,"y":0.037},"t":60,"s":[52.25,371.162,0],"to":[17.313,15.282,0],"ti":[-111.385,109.594,0]},{"t":120,"s":[254.591,233.94,0]}],"ix":2,"l":2},"a":{"a":0,"k":[17.55,17.625,0],"ix":1,"l":2},"s":{"a":0,"k":[118.243,118.243,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.27,"y":1},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[16.71,16.977],[15.715,15.982]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.73,"y":0},"t":10,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[17.218,17.05],[-5.447,-17.05]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[17.546,17.05],[17.273,-17.05]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":35,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[17.962,17.05],[46.065,-17.05]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":43,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[18.091,17.05],[54.956,-17.05]],"c":false}]},{"i":{"x":0.36,"y":1},"o":{"x":0.167,"y":0.167},"t":60,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[17.546,17.05],[17.273,-17.05]],"c":false}]},{"t":100,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[17.05,17.05],[-17.05,-17.05]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":1,"k":[{"i":{"x":0.27,"y":1},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[15.7,18.006],[16.725,16.982]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.73,"y":0},"t":10,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-5.779,52.3],[17.55,17.2]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[17.269,52.3],[17.55,17.2]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":35,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[46.477,52.3],[17.55,17.2]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":43,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[55.496,52.3],[17.55,17.2]],"c":false}]},{"i":{"x":0.36,"y":1},"o":{"x":0.167,"y":0.167},"t":60,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[17.269,52.3],[17.55,17.2]],"c":false}]},{"t":100,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-17.55,52.3],[17.55,17.2]],"c":false}]}],"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('232-arrow-21').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":10,"ix":5,"x":"var $bm_rt;\n$bm_rt = $bm_mul($bm_div(value, 2), comp('232-arrow-21').layer('control').effect('stroke')('Menu'));"},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Vector","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":600,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Vector","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-45,"ix":10},"p":{"a":0,"k":[212.41,212.41,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[118.243,118.243,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[12.8,0],[12.6,-0.5],[-0.1,-18.6],[-83.4,-4.2],[-15.5,0],[-4.133,0],[0,0],[0,15.1],[87.4,3.5]],"o":[[-13.3,0],[-86.5,3.6],[0,18.2],[14.5,0.7],[4.1,0],[41.4,0],[62.1,-6.4],[0,-18.8],[-12,-0.5]],"v":[[-0.05,-38.75],[-38.95,-37.95],[-190.45,-0.05],[-45.15,37.65],[-0.05,38.75],[12.35,38.65],[85.75,34.55],[190.45,-0.05],[37.15,-38.05]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.21],"y":[1]},"o":{"x":[0.28],"y":[0]},"t":0,"s":[0]},{"t":120,"s":[10]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.838],"y":[0.938]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"i":{"x":[0.667],"y":[0.862]},"o":{"x":[0.333],"y":[0.122]},"t":20,"s":[24]},{"i":{"x":[0.38],"y":[1]},"o":{"x":[0.233],"y":[0.277]},"t":60,"s":[74]},{"t":120,"s":[100]}],"ix":2},"o":{"a":0,"k":184,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('232-arrow-21').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":10,"ix":5,"x":"var $bm_rt;\n$bm_rt = $bm_mul($bm_div(value, 2), comp('232-arrow-21').layer('control').effect('stroke')('Menu'));"},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Vector","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":600,"st":0,"bm":0}]},{"id":"comp_1","nm":"hover-roll","fr":60,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Vector 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-45,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.454,"y":0.663},"o":{"x":0.333,"y":0},"t":0,"s":[254.591,233.94,0],"to":[91.728,-89.288,0],"ti":[15.825,17.58,0]},{"i":{"x":0.833,"y":0.839},"o":{"x":0.349,"y":0.126},"t":33,"s":[371.54,52.516,0],"to":[-13.998,-15.55,0],"ti":[108.509,-104.074,0]},{"i":{"x":0.609,"y":0.905},"o":{"x":0.167,"y":0.172},"t":52.439,"s":[190.63,169.978,0],"to":[-116.05,111.307,0],"ti":[-17.451,-15.403,0]},{"i":{"x":0.36,"y":1},"o":{"x":0.241,"y":0.111},"t":76,"s":[52.25,371.162,0],"to":[17.313,15.282,0],"ti":[-111.385,109.594,0]},{"t":120,"s":[254.591,233.94,0]}],"ix":2,"l":2},"a":{"a":0,"k":[17.55,17.625,0],"ix":1,"l":2},"s":{"a":0,"k":[118.243,118.243,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[17.05,17.05],[-17.05,-17.05]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":33,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[17.546,17.05],[17.273,-17.05]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":47,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[17.962,17.05],[46.065,-17.05]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":55,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[18.091,17.05],[54.956,-17.05]],"c":false}]},{"i":{"x":0.36,"y":1},"o":{"x":0.167,"y":0.167},"t":76,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[17.546,17.05],[17.273,-17.05]],"c":false}]},{"t":103,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[17.05,17.05],[-17.05,-17.05]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-17.55,52.3],[17.55,17.2]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":33,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[17.269,52.3],[17.55,17.2]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":47,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[46.477,52.3],[17.55,17.2]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":55,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[55.496,52.3],[17.55,17.2]],"c":false}]},{"i":{"x":0.36,"y":1},"o":{"x":0.167,"y":0.167},"t":76,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[17.269,52.3],[17.55,17.2]],"c":false}]},{"t":103,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-17.55,52.3],[17.55,17.2]],"c":false}]}],"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('232-arrow-21').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":10,"ix":5,"x":"var $bm_rt;\n$bm_rt = $bm_mul($bm_div(value, 2), comp('232-arrow-21').layer('control').effect('stroke')('Menu'));"},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Vector","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":600,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Vector","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-45,"ix":10},"p":{"a":0,"k":[212.41,212.41,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[118.243,118.243,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-13.3,0],[-12,-0.5],[0,-18.8],[62.1,-6.4],[41.4,0],[4.1,0],[14.5,0.7],[0,18.2],[-86.5,3.6]],"o":[[12.8,0],[87.4,3.5],[0,15.1],[0,0],[-4.133,0],[-15.5,0],[-83.4,-4.2],[-0.1,-18.6],[12.6,-0.5]],"v":[[-0.05,-38.75],[37.15,-38.05],[190.45,-0.05],[85.75,34.55],[12.35,38.65],[-0.05,38.75],[-45.15,37.65],[-190.45,-0.05],[-38.95,-37.95]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":0,"k":90,"ix":2},"o":{"a":1,"k":[{"i":{"x":[0.669],"y":[0.508]},"o":{"x":[0.357],"y":[0]},"t":0,"s":[177]},{"i":{"x":[0.647],"y":[0.648]},"o":{"x":[0.319],"y":[0.372]},"t":15,"s":[140.786]},{"i":{"x":[0.69],"y":[0.756]},"o":{"x":[0.353],"y":[0.325]},"t":23,"s":[116.18]},{"i":{"x":[0.787],"y":[0.738]},"o":{"x":[0.432],"y":[0.53]},"t":27,"s":[102.846]},{"i":{"x":[0.678],"y":[0.533]},"o":{"x":[0.345],"y":[0.448]},"t":33,"s":[90]},{"i":{"x":[0.686],"y":[0.714]},"o":{"x":[0.353],"y":[0.284]},"t":37,"s":[82.015]},{"i":{"x":[0.631],"y":[0.643]},"o":{"x":[0.286],"y":[0.172]},"t":43,"s":[60.409]},{"i":{"x":[0.664],"y":[0.74]},"o":{"x":[0.322],"y":[0.327]},"t":54,"s":[0.454]},{"i":{"x":[0.673],"y":[0.789]},"o":{"x":[0.338],"y":[0.369]},"t":66,"s":[-61.886]},{"i":{"x":[0.679],"y":[0.543]},"o":{"x":[0.343],"y":[0.528]},"t":71,"s":[-80.306]},{"i":{"x":[0.587],"y":[0.409]},"o":{"x":[0.243],"y":[0.215]},"t":76,"s":[-88]},{"i":{"x":[0.627],"y":[0.73]},"o":{"x":[0.279],"y":[0.281]},"t":83,"s":[-105.245]},{"i":{"x":[0.68],"y":[1]},"o":{"x":[0.326],"y":[0.726]},"t":98,"s":[-157.904]},{"t":120,"s":[-183]}],"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('232-arrow-21').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":10,"ix":5,"x":"var $bm_rt;\n$bm_rt = $bm_mul($bm_div(value, 2), comp('232-arrow-21').layer('control').effect('stroke')('Menu'));"},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Vector","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":600,"st":0,"bm":0}]},{"id":"comp_2","nm":"loop-roll","fr":60,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Vector 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-45,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.724,"y":0.939},"o":{"x":0.54,"y":0.486},"t":0,"s":[254.591,233.94,0],"to":[91.728,-89.288,0],"ti":[15.825,17.58,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.212,"y":0.043},"t":30,"s":[371.54,52.516,0],"to":[-13.998,-15.55,0],"ti":[108.509,-104.074,0]},{"i":{"x":0.777,"y":0.954},"o":{"x":0.167,"y":0.166},"t":57.334,"s":[190.63,169.978,0],"to":[-116.05,111.307,0],"ti":[-17.451,-15.403,0]},{"i":{"x":0.833,"y":0.856},"o":{"x":0.248,"y":0.053},"t":88,"s":[52.25,371.162,0],"to":[17.313,15.282,0],"ti":[-111.385,109.594,0]},{"t":120,"s":[254.591,233.94,0]}],"ix":2,"l":2},"a":{"a":0,"k":[17.55,17.625,0],"ix":1,"l":2},"s":{"a":0,"k":[118.243,118.243,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[17.05,17.05],[-17.05,-17.05]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[17.546,17.05],[17.273,-17.05]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":47,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[17.962,17.05],[46.065,-17.05]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":55,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[18.091,17.05],[54.956,-17.05]],"c":false}]},{"i":{"x":0.36,"y":1},"o":{"x":0.167,"y":0.167},"t":90,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[17.546,17.05],[17.273,-17.05]],"c":false}]},{"t":103,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[17.05,17.05],[-17.05,-17.05]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-17.55,52.3],[17.55,17.2]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[17.269,52.3],[17.55,17.2]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":47,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[46.477,52.3],[17.55,17.2]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":55,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[55.496,52.3],[17.55,17.2]],"c":false}]},{"i":{"x":0.36,"y":1},"o":{"x":0.167,"y":0.167},"t":90,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[17.269,52.3],[17.55,17.2]],"c":false}]},{"t":103,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-17.55,52.3],[17.55,17.2]],"c":false}]}],"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('232-arrow-21').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":10,"ix":5,"x":"var $bm_rt;\n$bm_rt = $bm_mul($bm_div(value, 2), comp('232-arrow-21').layer('control').effect('stroke')('Menu'));"},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Vector","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":600,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Vector","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-45,"ix":10},"p":{"a":0,"k":[212.41,212.41,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[118.243,118.243,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-13.3,0],[-12,-0.5],[0,-18.8],[62.1,-6.4],[41.4,0],[4.1,0],[14.5,0.7],[0,18.2],[-86.5,3.6]],"o":[[12.8,0],[87.4,3.5],[0,15.1],[0,0],[-4.133,0],[-15.5,0],[-83.4,-4.2],[-0.1,-18.6],[12.6,-0.5]],"v":[[-0.05,-38.75],[37.15,-38.05],[190.45,-0.05],[85.75,34.55],[12.35,38.65],[-0.05,38.75],[-45.15,37.65],[-190.45,-0.05],[-38.95,-37.95]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":0,"k":90,"ix":2},"o":{"a":1,"k":[{"i":{"x":[0.571],"y":[0.529]},"o":{"x":[0.185],"y":[0.176]},"t":0,"s":[177]},{"i":{"x":[0.763],"y":[0.944]},"o":{"x":[0.359],"y":[0.427]},"t":12,"s":[139.995]},{"i":{"x":[0.771],"y":[0.949]},"o":{"x":[0.25],"y":[0.057]},"t":30,"s":[88.727]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.306],"y":[0.068]},"t":89,"s":[-88.215]},{"t":120,"s":[-183]}],"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('232-arrow-21').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":10,"ix":5,"x":"var $bm_rt;\n$bm_rt = $bm_mul($bm_div(value, 2), comp('232-arrow-21').layer('control').effect('stroke')('Menu'));"},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Vector","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":600,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":3,"nm":"control","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"ef":[{"ty":5,"nm":"stroke","np":3,"mn":"Pseudo/@@CfAoPsZIQ6KIGy9s+DWp8w","ix":1,"en":1,"ef":[{"ty":7,"nm":"Menu","mn":"Pseudo/@@CfAoPsZIQ6KIGy9s+DWp8w-0001","ix":1,"v":{"a":0,"k":2,"ix":1}}]},{"ty":5,"nm":"primary","np":3,"mn":"ADBE Color Control","ix":2,"en":1,"ef":[{"ty":2,"nm":"Color","mn":"ADBE Color Control-0001","ix":1,"v":{"a":0,"k":[1,1,1],"ix":1}}]}],"ip":0,"op":381,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":0,"nm":"in-reveal","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[215,215,0],"ix":2,"l":2},"a":{"a":0,"k":[215,215,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":430,"h":430,"ip":0,"op":130,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":0,"nm":"hover-roll","refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[215,215,0],"ix":2,"l":2},"a":{"a":0,"k":[215,215,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":430,"h":430,"ip":130,"op":260,"st":130,"bm":0},{"ddd":0,"ind":4,"ty":0,"nm":"loop-roll","refId":"comp_2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[215,215,0],"ix":2,"l":2},"a":{"a":0,"k":[215,215,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":430,"h":430,"ip":260,"op":390,"st":260,"bm":0}],"markers":[{"tm":0,"cm":"in-reveal","dr":120},{"tm":130,"cm":"default:hover-roll","dr":120},{"tm":260,"cm":"loop-roll","dr":120}]} \ No newline at end of file diff --git a/frontend/src/components/v2/Stepper/Stepper.tsx b/frontend/src/components/v2/Stepper/Stepper.tsx index 8eabddaf6..8389121c0 100644 --- a/frontend/src/components/v2/Stepper/Stepper.tsx +++ b/frontend/src/components/v2/Stepper/Stepper.tsx @@ -32,7 +32,7 @@ export const Stepper = ({ activeStep, children, direction, className }: StepperP
{ return ( -
+
{title}
{description &&
{description}
}
diff --git a/frontend/src/layouts/AppLayout/AppLayout.tsx b/frontend/src/layouts/AppLayout/AppLayout.tsx index 74f289209..aefe6ef62 100644 --- a/frontend/src/layouts/AppLayout/AppLayout.tsx +++ b/frontend/src/layouts/AppLayout/AppLayout.tsx @@ -503,7 +503,7 @@ export const AppLayout = ({ children }: LayoutProps) => { isSelected={ router.asPath === `/project/${currentWorkspace?._id}/secret-rotation` } - icon="system-outline-189-domain-verification" + icon="rotation" > Secret rotation diff --git a/frontend/src/views/SecretRotationPage/SecretRotationPage.tsx b/frontend/src/views/SecretRotationPage/SecretRotationPage.tsx index b83e30bde..e36f8d791 100644 --- a/frontend/src/views/SecretRotationPage/SecretRotationPage.tsx +++ b/frontend/src/views/SecretRotationPage/SecretRotationPage.tsx @@ -163,10 +163,10 @@ export const SecretRotationPage = withProjectPermission( }; const handleCreateRotation = async (provider: TSecretRotationProvider) => { - if (subscription && !subscription?.secretRotation) { - handlePopUpOpen("upgradePlan"); - return; - } + // if (subscription && !subscription?.secretRotation) { + // handlePopUpOpen("upgradePlan"); + // return; + // } if (!canCreateRotation) { createNotification({ type: "error", text: "Access permission denied!!" }); return; diff --git a/frontend/src/views/SecretRotationPage/components/CreateRotationForm/CreateRotationForm.tsx b/frontend/src/views/SecretRotationPage/components/CreateRotationForm/CreateRotationForm.tsx index c5493159e..3576cc0f9 100644 --- a/frontend/src/views/SecretRotationPage/components/CreateRotationForm/CreateRotationForm.tsx +++ b/frontend/src/views/SecretRotationPage/components/CreateRotationForm/CreateRotationForm.tsx @@ -7,7 +7,6 @@ import { TSecretRotationProvider } from "@app/hooks/api/types"; import { useNotificationContext } from "~/components/context/Notifications/NotificationProvider"; -import { GeneralDetailsForm, TFormSchema as TGeneralFormSchema } from "./steps/GeneralDetailsForm"; import { RotationInputForm } from "./steps/RotationInputForm"; import { RotationOutputForm, @@ -16,13 +15,12 @@ import { const WIZARD_STEPS = [ { - title: "General" + title: "Inputs", + description: "Provider secrets" }, { - title: "Inputs" - }, - { - title: "Secret Mapping" + title: "Outputs", + description: "Map rotated secrets to keys" } ]; @@ -43,7 +41,6 @@ export const CreateRotationForm = ({ }: Props) => { const [wizardStep, setWizardStep] = useState(0); const wizardData = useRef<{ - general?: TGeneralFormSchema; input?: Record; output?: TRotationOutputSchema; }>({}); @@ -58,18 +55,17 @@ export const CreateRotationForm = ({ }; const handleFormSubmit = async () => { - if (!wizardData.current.general || !wizardData.current.input || !wizardData.current.output) - return; + if (!wizardData.current.input || !wizardData.current.output) return; try { await createSecretRotation({ workspaceId, provider: provider.name, customProvider, - secretPath: wizardData.current.general.secretPath, - environment: wizardData.current.general.environment, - interval: wizardData.current.general.interval, + secretPath: wizardData.current.output.secretPath, + environment: wizardData.current.output.environment, + interval: wizardData.current.output.interval, inputs: wizardData.current.input, - outputs: wizardData.current.output + outputs: wizardData.current.output.secrets }); setWizardStep(0); onToggle(false); @@ -98,49 +94,50 @@ export const CreateRotationForm = ({ className="max-w-2xl" > - {WIZARD_STEPS.map(({ title }, index) => ( - + {WIZARD_STEPS.map(({ title, description }, index) => ( + ))} {wizardStep === 0 && ( - { - wizardData.current.general = data; + wizardData.current.input = data; setWizardStep((state) => state + 1); }} + inputSchema={provider.template?.inputs || {}} /> )} {wizardStep === 1 && ( - { - wizardData.current.input = data; - setWizardStep((state) => state + 1); - }} - inputSchema={provider.template?.inputs || {}} - /> - )} - {wizardStep === 2 && ( - { - wizardData.current.output = data; - await handleFormSubmit(); - }} - /> + + { + wizardData.current.output = data; + await handleFormSubmit(); + }} + /> + )} diff --git a/frontend/src/views/SecretRotationPage/components/CreateRotationForm/steps/GeneralDetailsForm.tsx b/frontend/src/views/SecretRotationPage/components/CreateRotationForm/steps/GeneralDetailsForm.tsx deleted file mode 100644 index b83b19974..000000000 --- a/frontend/src/views/SecretRotationPage/components/CreateRotationForm/steps/GeneralDetailsForm.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import { Controller, useForm } from "react-hook-form"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { z } from "zod"; - -import { Button, FormControl, Input, Select, SelectItem } from "@app/components/v2"; -import { useWorkspace } from "@app/context"; - -const formSchema = z.object({ - environment: z.string().trim(), - secretPath: z.string().trim().default("/"), - interval: z.number() -}); - -export type TFormSchema = z.infer; -type Props = { - onSubmit: (data: TFormSchema) => void; - onCancel: () => void; -}; - -export const GeneralDetailsForm = ({ onSubmit, onCancel }: Props) => { - const { currentWorkspace } = useWorkspace(); - const environments = currentWorkspace?.environments || []; - const { - control, - handleSubmit, - formState: { isSubmitting } - } = useForm({ - resolver: zodResolver(formSchema) - }); - - return ( -
- ( - - - - )} - /> - ( - - - - )} - /> - ( - - field.onChange(parseInt(evt.target.value, 10))} - /> - - )} - /> -
- - -
- - ); -}; diff --git a/frontend/src/views/SecretRotationPage/components/CreateRotationForm/steps/RotationInputForm.tsx b/frontend/src/views/SecretRotationPage/components/CreateRotationForm/steps/RotationInputForm.tsx index baacfc2a5..8c50687d1 100644 --- a/frontend/src/views/SecretRotationPage/components/CreateRotationForm/steps/RotationInputForm.tsx +++ b/frontend/src/views/SecretRotationPage/components/CreateRotationForm/steps/RotationInputForm.tsx @@ -34,7 +34,6 @@ export const RotationInputForm = ({ onSubmit, onCancel, inputSchema }: Props) => defaultValue={inputSchema.properties[inputName]?.default} render={({ field }) => ( ; type Props = { - environment: string; - secretPath: string; outputSchema: Record; onSubmit: (data: TFormSchema) => void; onCancel: () => void; }; -export const RotationOutputForm = ({ - onSubmit, - onCancel, - environment, - secretPath, - outputSchema = {} -}: Props) => { +export const RotationOutputForm = ({ onSubmit, onCancel, outputSchema = {} }: Props) => { const { currentWorkspace } = useWorkspace(); + const environments = currentWorkspace?.environments || []; const workspaceId = currentWorkspace?._id || ""; const { control, handleSubmit, + watch, formState: { isSubmitting } } = useForm({ resolver: zodResolver(formSchema) }); + const environment = watch("environment", environments?.[0]?.slug); + const secretPath = watch("secretPath"); + const { data: userWsKey } = useGetUserWsKey(workspaceId); - const { data: secrets } = useGetProjectSecrets({ + const { data: secrets, isLoading: isSecretsLoading } = useGetProjectSecrets({ workspaceId, environment, secretPath, @@ -44,11 +46,65 @@ export const RotationOutputForm = ({ return (
+ ( + + + + )} + /> + ( + + + + )} + /> + ( + + field.onChange(parseInt(evt.target.value, 10))} + /> + + )} + /> +
+
Mapping
+
Select keys for rotated value to get saved
+
{Object.keys(outputSchema).map((outputName) => ( ( )} From 9248f36edb6c8b4b911f0aa1f02ee74c13ba73b0 Mon Sep 17 00:00:00 2001 From: Akhil Mohan Date: Thu, 26 Oct 2023 20:17:18 +0530 Subject: [PATCH 15/33] feat(secret-rotation): added db ssl option in test function --- backend/src/ee/secretRotation/templates/mysql.ts | 1 + backend/src/ee/secretRotation/templates/postgres.ts | 1 + .../views/SecretRotationPage/SecretRotationPage.tsx | 10 +++++----- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/backend/src/ee/secretRotation/templates/mysql.ts b/backend/src/ee/secretRotation/templates/mysql.ts index fa730b881..b57666596 100644 --- a/backend/src/ee/secretRotation/templates/mysql.ts +++ b/backend/src/ee/secretRotation/templates/mysql.ts @@ -69,6 +69,7 @@ export const MYSQL_TEMPLATE = { host: "${inputs.host}", database: "${inputs.database}", port: "${inputs.port}", + ca: "${inputs.ca}", query: "SELECT NOW()" } } diff --git a/backend/src/ee/secretRotation/templates/postgres.ts b/backend/src/ee/secretRotation/templates/postgres.ts index ca489570b..c4981cf29 100644 --- a/backend/src/ee/secretRotation/templates/postgres.ts +++ b/backend/src/ee/secretRotation/templates/postgres.ts @@ -68,6 +68,7 @@ export const POSTGRES_TEMPLATE = { host: "${inputs.host}", database: "${inputs.database}", port: "${inputs.port}", + ca: "${inputs.ca}", query: "SELECT NOW()" } } diff --git a/frontend/src/views/SecretRotationPage/SecretRotationPage.tsx b/frontend/src/views/SecretRotationPage/SecretRotationPage.tsx index e36f8d791..0cb179db3 100644 --- a/frontend/src/views/SecretRotationPage/SecretRotationPage.tsx +++ b/frontend/src/views/SecretRotationPage/SecretRotationPage.tsx @@ -163,10 +163,10 @@ export const SecretRotationPage = withProjectPermission( }; const handleCreateRotation = async (provider: TSecretRotationProvider) => { - // if (subscription && !subscription?.secretRotation) { - // handlePopUpOpen("upgradePlan"); - // return; - // } + if (subscription && !subscription?.secretRotation) { + handlePopUpOpen("upgradePlan"); + return; + } if (!canCreateRotation) { createNotification({ type: "error", text: "Access permission denied!!" }); return; @@ -392,7 +392,7 @@ export const SecretRotationPage = withProjectPermission( title="Are you sure want to delete this rotation?" subTitle="This will stop the rotation from dynamically changing. Secret won't be deleted" onChange={(isOpen) => handlePopUpToggle("deleteRotation", isOpen)} - deleteKey="confirm" + deleteKey="delete" onDeleteApproved={handleDeleteRotation} /> Date: Thu, 26 Oct 2023 12:35:44 -0400 Subject: [PATCH 16/33] add secretRotation to feature set --- backend/src/ee/services/EELicenseService.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/backend/src/ee/services/EELicenseService.ts b/backend/src/ee/services/EELicenseService.ts index f403f55d6..6baea04dc 100644 --- a/backend/src/ee/services/EELicenseService.ts +++ b/backend/src/ee/services/EELicenseService.ts @@ -38,6 +38,7 @@ interface FeatureSet { trial_end: number | null; has_used_trial: boolean; secretApproval: boolean; + secretRotation: boolean; } /** @@ -74,7 +75,8 @@ class EELicenseService { status: null, trial_end: null, has_used_trial: true, - secretApproval: false + secretApproval: false, + secretRotation: true, } public localFeatureSet: NodeCache; From bc68a002659045703cd9c3279f0298305bfb5e9d Mon Sep 17 00:00:00 2001 From: Akhil Mohan Date: Fri, 27 Oct 2023 15:00:51 +0530 Subject: [PATCH 17/33] feat(secret-rotation): updated lottie and added side effect on successfully --- backend/src/controllers/v3/secretsController.ts | 16 ++++++++++++++++ backend/src/ee/secretRotation/queue.ts | 13 +++++++++++-- frontend/public/lotties/rotation.json | 2 +- frontend/src/layouts/AppLayout/AppLayout.tsx | 4 ++-- 4 files changed, 30 insertions(+), 5 deletions(-) diff --git a/backend/src/controllers/v3/secretsController.ts b/backend/src/controllers/v3/secretsController.ts index 31b645432..dedb15b65 100644 --- a/backend/src/controllers/v3/secretsController.ts +++ b/backend/src/controllers/v3/secretsController.ts @@ -859,6 +859,14 @@ export const createSecretByNameBatch = async (req: Request, res: Response) => { authData: req.authData }); + await EventService.handleEvent({ + event: eventPushSecrets({ + workspaceId: new Types.ObjectId(workspaceId), + environment, + secretPath + }) + }); + return res.status(200).send({ secrets: createdSecrets }); @@ -903,6 +911,14 @@ export const updateSecretByNameBatch = async (req: Request, res: Response) => { authData: req.authData }); + await EventService.handleEvent({ + event: eventPushSecrets({ + workspaceId: new Types.ObjectId(workspaceId), + environment, + secretPath + }) + }); + return res.status(200).send({ secrets: updatedSecrets }); diff --git a/backend/src/ee/secretRotation/queue.ts b/backend/src/ee/secretRotation/queue.ts index e85cb88ec..c545a9ac2 100644 --- a/backend/src/ee/secretRotation/queue.ts +++ b/backend/src/ee/secretRotation/queue.ts @@ -6,7 +6,7 @@ import { Client as PgClient } from "pg"; import mysql from "mysql"; import { client, getRootEncryptionKey } from "../../config"; -import { BotService, TelemetryService } from "../../services"; +import { BotService, EventService, TelemetryService } from "../../services"; import { SecretRotation } from "./models"; import { rotationTemplates } from "./templates"; import { @@ -26,6 +26,7 @@ import { ISecret, Secret } from "../../models"; import { SECRET_SHARED } from "../../variables"; import { EESecretService } from "../services"; import { SecretVersion } from "../models"; +import { eventPushSecrets } from "../../events"; const REGEX = /\${([^}]+)}/g; const SLUG_ALPHABETS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; @@ -380,8 +381,15 @@ secretRotationQueue.process(async (job: Job) => { folderId }); - const postHogClient = await TelemetryService.getPostHogClient(); + await EventService.handleEvent({ + event: eventPushSecrets({ + workspaceId: secretRotation.workspace, + environment: secretRotation.environment, + secretPath: secretRotation.secretPath + }) + }); + const postHogClient = await TelemetryService.getPostHogClient(); if (postHogClient) { postHogClient.capture({ event: "secrets rotated", @@ -401,6 +409,7 @@ secretRotationQueue.process(async (job: Job) => { lastRotatedAt: new Date().toUTCString() }); } + return Promise.resolve(); }); diff --git a/frontend/public/lotties/rotation.json b/frontend/public/lotties/rotation.json index 5ea9f1551..d05d254a4 100644 --- a/frontend/public/lotties/rotation.json +++ b/frontend/public/lotties/rotation.json @@ -1 +1 @@ -{"v":"5.8.1","fr":60,"ip":130,"op":250,"w":430,"h":430,"nm":"232-arrow-21","ddd":0,"assets":[{"id":"comp_0","nm":"in-reveal","fr":60,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Vector 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-45,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.724,"y":0.959},"o":{"x":0.249,"y":0.15},"t":0,"s":[254.591,233.94,0],"to":[91.728,-89.288,0],"ti":[15.825,17.58,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.212,"y":0.03},"t":20,"s":[371.54,52.516,0],"to":[-13.998,-15.55,0],"ti":[108.509,-104.074,0]},{"i":{"x":0.777,"y":0.968},"o":{"x":0.167,"y":0.166},"t":38.854,"s":[190.63,169.978,0],"to":[-116.05,111.307,0],"ti":[-17.451,-15.403,0]},{"i":{"x":0.34,"y":1},"o":{"x":0.092,"y":0.037},"t":60,"s":[52.25,371.162,0],"to":[17.313,15.282,0],"ti":[-111.385,109.594,0]},{"t":120,"s":[254.591,233.94,0]}],"ix":2,"l":2},"a":{"a":0,"k":[17.55,17.625,0],"ix":1,"l":2},"s":{"a":0,"k":[118.243,118.243,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.27,"y":1},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[16.71,16.977],[15.715,15.982]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.73,"y":0},"t":10,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[17.218,17.05],[-5.447,-17.05]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[17.546,17.05],[17.273,-17.05]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":35,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[17.962,17.05],[46.065,-17.05]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":43,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[18.091,17.05],[54.956,-17.05]],"c":false}]},{"i":{"x":0.36,"y":1},"o":{"x":0.167,"y":0.167},"t":60,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[17.546,17.05],[17.273,-17.05]],"c":false}]},{"t":100,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[17.05,17.05],[-17.05,-17.05]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":1,"k":[{"i":{"x":0.27,"y":1},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[15.7,18.006],[16.725,16.982]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.73,"y":0},"t":10,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-5.779,52.3],[17.55,17.2]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[17.269,52.3],[17.55,17.2]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":35,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[46.477,52.3],[17.55,17.2]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":43,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[55.496,52.3],[17.55,17.2]],"c":false}]},{"i":{"x":0.36,"y":1},"o":{"x":0.167,"y":0.167},"t":60,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[17.269,52.3],[17.55,17.2]],"c":false}]},{"t":100,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-17.55,52.3],[17.55,17.2]],"c":false}]}],"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('232-arrow-21').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":10,"ix":5,"x":"var $bm_rt;\n$bm_rt = $bm_mul($bm_div(value, 2), comp('232-arrow-21').layer('control').effect('stroke')('Menu'));"},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Vector","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":600,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Vector","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-45,"ix":10},"p":{"a":0,"k":[212.41,212.41,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[118.243,118.243,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[12.8,0],[12.6,-0.5],[-0.1,-18.6],[-83.4,-4.2],[-15.5,0],[-4.133,0],[0,0],[0,15.1],[87.4,3.5]],"o":[[-13.3,0],[-86.5,3.6],[0,18.2],[14.5,0.7],[4.1,0],[41.4,0],[62.1,-6.4],[0,-18.8],[-12,-0.5]],"v":[[-0.05,-38.75],[-38.95,-37.95],[-190.45,-0.05],[-45.15,37.65],[-0.05,38.75],[12.35,38.65],[85.75,34.55],[190.45,-0.05],[37.15,-38.05]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.21],"y":[1]},"o":{"x":[0.28],"y":[0]},"t":0,"s":[0]},{"t":120,"s":[10]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.838],"y":[0.938]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"i":{"x":[0.667],"y":[0.862]},"o":{"x":[0.333],"y":[0.122]},"t":20,"s":[24]},{"i":{"x":[0.38],"y":[1]},"o":{"x":[0.233],"y":[0.277]},"t":60,"s":[74]},{"t":120,"s":[100]}],"ix":2},"o":{"a":0,"k":184,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('232-arrow-21').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":10,"ix":5,"x":"var $bm_rt;\n$bm_rt = $bm_mul($bm_div(value, 2), comp('232-arrow-21').layer('control').effect('stroke')('Menu'));"},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Vector","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":600,"st":0,"bm":0}]},{"id":"comp_1","nm":"hover-roll","fr":60,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Vector 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-45,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.454,"y":0.663},"o":{"x":0.333,"y":0},"t":0,"s":[254.591,233.94,0],"to":[91.728,-89.288,0],"ti":[15.825,17.58,0]},{"i":{"x":0.833,"y":0.839},"o":{"x":0.349,"y":0.126},"t":33,"s":[371.54,52.516,0],"to":[-13.998,-15.55,0],"ti":[108.509,-104.074,0]},{"i":{"x":0.609,"y":0.905},"o":{"x":0.167,"y":0.172},"t":52.439,"s":[190.63,169.978,0],"to":[-116.05,111.307,0],"ti":[-17.451,-15.403,0]},{"i":{"x":0.36,"y":1},"o":{"x":0.241,"y":0.111},"t":76,"s":[52.25,371.162,0],"to":[17.313,15.282,0],"ti":[-111.385,109.594,0]},{"t":120,"s":[254.591,233.94,0]}],"ix":2,"l":2},"a":{"a":0,"k":[17.55,17.625,0],"ix":1,"l":2},"s":{"a":0,"k":[118.243,118.243,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[17.05,17.05],[-17.05,-17.05]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":33,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[17.546,17.05],[17.273,-17.05]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":47,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[17.962,17.05],[46.065,-17.05]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":55,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[18.091,17.05],[54.956,-17.05]],"c":false}]},{"i":{"x":0.36,"y":1},"o":{"x":0.167,"y":0.167},"t":76,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[17.546,17.05],[17.273,-17.05]],"c":false}]},{"t":103,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[17.05,17.05],[-17.05,-17.05]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-17.55,52.3],[17.55,17.2]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":33,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[17.269,52.3],[17.55,17.2]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":47,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[46.477,52.3],[17.55,17.2]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":55,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[55.496,52.3],[17.55,17.2]],"c":false}]},{"i":{"x":0.36,"y":1},"o":{"x":0.167,"y":0.167},"t":76,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[17.269,52.3],[17.55,17.2]],"c":false}]},{"t":103,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-17.55,52.3],[17.55,17.2]],"c":false}]}],"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('232-arrow-21').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":10,"ix":5,"x":"var $bm_rt;\n$bm_rt = $bm_mul($bm_div(value, 2), comp('232-arrow-21').layer('control').effect('stroke')('Menu'));"},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Vector","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":600,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Vector","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-45,"ix":10},"p":{"a":0,"k":[212.41,212.41,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[118.243,118.243,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-13.3,0],[-12,-0.5],[0,-18.8],[62.1,-6.4],[41.4,0],[4.1,0],[14.5,0.7],[0,18.2],[-86.5,3.6]],"o":[[12.8,0],[87.4,3.5],[0,15.1],[0,0],[-4.133,0],[-15.5,0],[-83.4,-4.2],[-0.1,-18.6],[12.6,-0.5]],"v":[[-0.05,-38.75],[37.15,-38.05],[190.45,-0.05],[85.75,34.55],[12.35,38.65],[-0.05,38.75],[-45.15,37.65],[-190.45,-0.05],[-38.95,-37.95]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":0,"k":90,"ix":2},"o":{"a":1,"k":[{"i":{"x":[0.669],"y":[0.508]},"o":{"x":[0.357],"y":[0]},"t":0,"s":[177]},{"i":{"x":[0.647],"y":[0.648]},"o":{"x":[0.319],"y":[0.372]},"t":15,"s":[140.786]},{"i":{"x":[0.69],"y":[0.756]},"o":{"x":[0.353],"y":[0.325]},"t":23,"s":[116.18]},{"i":{"x":[0.787],"y":[0.738]},"o":{"x":[0.432],"y":[0.53]},"t":27,"s":[102.846]},{"i":{"x":[0.678],"y":[0.533]},"o":{"x":[0.345],"y":[0.448]},"t":33,"s":[90]},{"i":{"x":[0.686],"y":[0.714]},"o":{"x":[0.353],"y":[0.284]},"t":37,"s":[82.015]},{"i":{"x":[0.631],"y":[0.643]},"o":{"x":[0.286],"y":[0.172]},"t":43,"s":[60.409]},{"i":{"x":[0.664],"y":[0.74]},"o":{"x":[0.322],"y":[0.327]},"t":54,"s":[0.454]},{"i":{"x":[0.673],"y":[0.789]},"o":{"x":[0.338],"y":[0.369]},"t":66,"s":[-61.886]},{"i":{"x":[0.679],"y":[0.543]},"o":{"x":[0.343],"y":[0.528]},"t":71,"s":[-80.306]},{"i":{"x":[0.587],"y":[0.409]},"o":{"x":[0.243],"y":[0.215]},"t":76,"s":[-88]},{"i":{"x":[0.627],"y":[0.73]},"o":{"x":[0.279],"y":[0.281]},"t":83,"s":[-105.245]},{"i":{"x":[0.68],"y":[1]},"o":{"x":[0.326],"y":[0.726]},"t":98,"s":[-157.904]},{"t":120,"s":[-183]}],"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('232-arrow-21').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":10,"ix":5,"x":"var $bm_rt;\n$bm_rt = $bm_mul($bm_div(value, 2), comp('232-arrow-21').layer('control').effect('stroke')('Menu'));"},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Vector","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":600,"st":0,"bm":0}]},{"id":"comp_2","nm":"loop-roll","fr":60,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Vector 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-45,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.724,"y":0.939},"o":{"x":0.54,"y":0.486},"t":0,"s":[254.591,233.94,0],"to":[91.728,-89.288,0],"ti":[15.825,17.58,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.212,"y":0.043},"t":30,"s":[371.54,52.516,0],"to":[-13.998,-15.55,0],"ti":[108.509,-104.074,0]},{"i":{"x":0.777,"y":0.954},"o":{"x":0.167,"y":0.166},"t":57.334,"s":[190.63,169.978,0],"to":[-116.05,111.307,0],"ti":[-17.451,-15.403,0]},{"i":{"x":0.833,"y":0.856},"o":{"x":0.248,"y":0.053},"t":88,"s":[52.25,371.162,0],"to":[17.313,15.282,0],"ti":[-111.385,109.594,0]},{"t":120,"s":[254.591,233.94,0]}],"ix":2,"l":2},"a":{"a":0,"k":[17.55,17.625,0],"ix":1,"l":2},"s":{"a":0,"k":[118.243,118.243,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[17.05,17.05],[-17.05,-17.05]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[17.546,17.05],[17.273,-17.05]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":47,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[17.962,17.05],[46.065,-17.05]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":55,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[18.091,17.05],[54.956,-17.05]],"c":false}]},{"i":{"x":0.36,"y":1},"o":{"x":0.167,"y":0.167},"t":90,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[17.546,17.05],[17.273,-17.05]],"c":false}]},{"t":103,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[17.05,17.05],[-17.05,-17.05]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-17.55,52.3],[17.55,17.2]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[17.269,52.3],[17.55,17.2]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":47,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[46.477,52.3],[17.55,17.2]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":55,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[55.496,52.3],[17.55,17.2]],"c":false}]},{"i":{"x":0.36,"y":1},"o":{"x":0.167,"y":0.167},"t":90,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[17.269,52.3],[17.55,17.2]],"c":false}]},{"t":103,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-17.55,52.3],[17.55,17.2]],"c":false}]}],"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('232-arrow-21').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":10,"ix":5,"x":"var $bm_rt;\n$bm_rt = $bm_mul($bm_div(value, 2), comp('232-arrow-21').layer('control').effect('stroke')('Menu'));"},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Vector","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":600,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Vector","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-45,"ix":10},"p":{"a":0,"k":[212.41,212.41,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[118.243,118.243,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-13.3,0],[-12,-0.5],[0,-18.8],[62.1,-6.4],[41.4,0],[4.1,0],[14.5,0.7],[0,18.2],[-86.5,3.6]],"o":[[12.8,0],[87.4,3.5],[0,15.1],[0,0],[-4.133,0],[-15.5,0],[-83.4,-4.2],[-0.1,-18.6],[12.6,-0.5]],"v":[[-0.05,-38.75],[37.15,-38.05],[190.45,-0.05],[85.75,34.55],[12.35,38.65],[-0.05,38.75],[-45.15,37.65],[-190.45,-0.05],[-38.95,-37.95]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":0,"k":90,"ix":2},"o":{"a":1,"k":[{"i":{"x":[0.571],"y":[0.529]},"o":{"x":[0.185],"y":[0.176]},"t":0,"s":[177]},{"i":{"x":[0.763],"y":[0.944]},"o":{"x":[0.359],"y":[0.427]},"t":12,"s":[139.995]},{"i":{"x":[0.771],"y":[0.949]},"o":{"x":[0.25],"y":[0.057]},"t":30,"s":[88.727]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.306],"y":[0.068]},"t":89,"s":[-88.215]},{"t":120,"s":[-183]}],"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('232-arrow-21').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":10,"ix":5,"x":"var $bm_rt;\n$bm_rt = $bm_mul($bm_div(value, 2), comp('232-arrow-21').layer('control').effect('stroke')('Menu'));"},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Vector","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":600,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":3,"nm":"control","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"ef":[{"ty":5,"nm":"stroke","np":3,"mn":"Pseudo/@@CfAoPsZIQ6KIGy9s+DWp8w","ix":1,"en":1,"ef":[{"ty":7,"nm":"Menu","mn":"Pseudo/@@CfAoPsZIQ6KIGy9s+DWp8w-0001","ix":1,"v":{"a":0,"k":2,"ix":1}}]},{"ty":5,"nm":"primary","np":3,"mn":"ADBE Color Control","ix":2,"en":1,"ef":[{"ty":2,"nm":"Color","mn":"ADBE Color Control-0001","ix":1,"v":{"a":0,"k":[1,1,1],"ix":1}}]}],"ip":0,"op":381,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":0,"nm":"in-reveal","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[215,215,0],"ix":2,"l":2},"a":{"a":0,"k":[215,215,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":430,"h":430,"ip":0,"op":130,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":0,"nm":"hover-roll","refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[215,215,0],"ix":2,"l":2},"a":{"a":0,"k":[215,215,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":430,"h":430,"ip":130,"op":260,"st":130,"bm":0},{"ddd":0,"ind":4,"ty":0,"nm":"loop-roll","refId":"comp_2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[215,215,0],"ix":2,"l":2},"a":{"a":0,"k":[215,215,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":430,"h":430,"ip":260,"op":390,"st":260,"bm":0}],"markers":[{"tm":0,"cm":"in-reveal","dr":120},{"tm":130,"cm":"default:hover-roll","dr":120},{"tm":260,"cm":"loop-roll","dr":120}]} \ No newline at end of file +{"v":"5.12.1","fr":60,"ip":70,"op":130,"w":500,"h":500,"nm":"system-regular-18-autorenew","ddd":0,"assets":[{"id":"comp_0","nm":"in-autorenew","fr":60,"layers":[{"ddd":0,"ind":1,"ty":3,"nm":"NULL ","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.121],"y":[1]},"o":{"x":[0.089],"y":[0.223]},"t":0,"s":[0]},{"t":59,"s":[-360]}],"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2,"l":2},"a":{"a":0,"k":[50,50,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".primary.design","cl":"primary design","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[56.365,-32.706,0],"ix":2,"l":2},"a":{"a":0,"k":[256.365,167.294,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[36.203,36.206],[-36.201,36.206],[-36.203,-36.206]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-18-autorenew').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[149.137,136.205],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[-54.792,0],[-7.083,-76.459]],"o":[[0,0],[26.458,-43.959],[78.542,0],[0,0]],"v":[[-139.584,5.208],[-139.584,5],[-10.208,-68.125],[139.584,68.125]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":1,"s":[0]},{"t":9,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-18-autorenew').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[260.212,167.294],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":3,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":60,"st":1,"ct":1,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".primary.design","cl":"primary design","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[43.319,132.712,0],"ix":2,"l":2},"a":{"a":0,"k":[243.319,332.712,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-36.203,-36.206],[36.201,-36.206],[36.203,36.206]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-18-autorenew').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[350.223,363.801],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[54.792,0],[7.083,76.459]],"o":[[0,0],[-26.458,43.959],[-78.542,0],[0,0]],"v":[[139.584,-5.208],[139.584,-5],[10.208,68.125],[-139.584,-68.125]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-18-autorenew').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":1,"s":[0]},{"t":9,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":3,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"tr","p":{"a":0,"k":[239.795,332.712],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":3,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":60,"st":1,"ct":1,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250.002,250.002,0],"ix":2,"l":2},"a":{"a":0,"k":[250,250,0],"ix":1,"l":2},"s":{"a":0,"k":[2083,2083,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.41,0],[0,0],[0,-0.41],[-0.42,0],[0,0],[1.83,0],[0.31,3.35],[0.41,-0.05],[-0.04,-0.42],[-4.17,0],[-1.5,1.58],[0,0],[-0.42,0],[0,0.41],[0,0]],"o":[[0,0],[-0.42,0],[0,0.41],[0,0],[-1.22,1.28],[-3.39,0],[-0.04,-0.41],[-0.42,0.04],[0.38,4.12],[2.22,0],[0,0],[0,0.41],[0.42,0],[0,0],[-0.01,-0.42]],"v":[[6.917,-0.992],[3.417,-0.992],[2.657,-0.242],[3.417,0.508],[5.047,0.508],[0.317,2.528],[-6.173,-3.342],[-6.993,-4.022],[-7.673,-3.202],[0.317,4.028],[6.157,1.508],[6.157,3.238],[6.917,3.988],[7.677,3.238],[7.677,-0.242]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-18-autorenew').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Fill","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[249.683,253.972],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[4.17,0],[1.51,-1.59],[0,0],[0.42,0],[0,-0.41],[0,0],[-0.42,0],[0,0],[0,0.41],[0.42,0],[0,0],[-1.83,0],[-0.31,-3.35],[-0.38,0],[-0.02,0],[0.04,0.42]],"o":[[-2.24,0],[0,0],[0,-0.41],[-0.42,0],[0,0],[0,0.41],[0,0],[0.42,0],[0,-0.41],[0,0],[1.21,-1.28],[3.39,0],[0.04,0.39],[0.02,0],[0.42,-0.04],[-0.38,-4.12]],"v":[[-0.307,-4.025],[-6.177,-1.475],[-6.177,-3.235],[-6.937,-3.985],[-7.687,-3.235],[-7.687,0.245],[-6.927,0.995],[-3.427,0.995],[-2.667,0.245],[-3.427,-0.505],[-5.027,-0.505],[-0.307,-2.525],[6.183,3.345],[6.933,4.025],[7.003,4.025],[7.683,3.205]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-18-autorenew').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Fill","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[250.307,246.025],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[250.307,246.025],"ix":2},"a":{"a":0,"k":[250.307,246.025],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":60,"op":300,"st":0,"ct":1,"bm":0}]},{"id":"comp_1","nm":"hover-autorenew","fr":60,"layers":[{"ddd":0,"ind":1,"ty":3,"nm":"NULL ","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.15],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":1,"s":[0]},{"t":60,"s":[-360]}],"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2,"l":2},"a":{"a":0,"k":[50,50,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":1,"s":[101,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":4,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":40,"s":[100,100,100]},{"t":60,"s":[101,100,100]}],"ix":6,"l":2}},"ao":0,"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".primary.design","cl":"primary design","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[56.365,-32.706,0],"ix":2,"l":2},"a":{"a":0,"k":[256.365,167.294,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[36.203,36.206],[-36.201,36.206],[-36.203,-36.206]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-18-autorenew').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[149.137,136.205],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[-54.792,0],[-7.083,-76.459]],"o":[[0,0],[26.458,-43.959],[78.542,0],[0,0]],"v":[[-139.584,5.208],[-139.584,5],[-10.208,-68.125],[139.584,68.125]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-18-autorenew').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[260.212,167.294],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":3,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":60,"st":1,"ct":1,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".primary.design","cl":"primary design","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[43.319,132.712,0],"ix":2,"l":2},"a":{"a":0,"k":[243.319,332.712,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-36.203,-36.206],[36.201,-36.206],[36.203,36.206]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-18-autorenew').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[350.223,363.801],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[54.792,0],[7.083,76.459]],"o":[[0,0],[-26.458,43.959],[-78.542,0],[0,0]],"v":[[139.584,-5.208],[139.584,-5],[10.208,68.125],[-139.584,-68.125]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-18-autorenew').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":3,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"tr","p":{"a":0,"k":[239.795,332.712],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":3,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":60,"st":1,"ct":1,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250.002,250.002,0],"ix":2,"l":2},"a":{"a":0,"k":[250,250,0],"ix":1,"l":2},"s":{"a":0,"k":[2083,2083,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.41,0],[0,0],[0,-0.41],[-0.42,0],[0,0],[1.83,0],[0.31,3.35],[0.41,-0.05],[-0.04,-0.42],[-4.17,0],[-1.5,1.58],[0,0],[-0.42,0],[0,0.41],[0,0]],"o":[[0,0],[-0.42,0],[0,0.41],[0,0],[-1.22,1.28],[-3.39,0],[-0.04,-0.41],[-0.42,0.04],[0.38,4.12],[2.22,0],[0,0],[0,0.41],[0.42,0],[0,0],[-0.01,-0.42]],"v":[[6.917,-0.992],[3.417,-0.992],[2.657,-0.242],[3.417,0.508],[5.047,0.508],[0.317,2.528],[-6.173,-3.342],[-6.993,-4.022],[-7.673,-3.202],[0.317,4.028],[6.157,1.508],[6.157,3.238],[6.917,3.988],[7.677,3.238],[7.677,-0.242]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-18-autorenew').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Fill","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[249.683,253.972],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[4.17,0],[1.51,-1.59],[0,0],[0.42,0],[0,-0.41],[0,0],[-0.42,0],[0,0],[0,0.41],[0.42,0],[0,0],[-1.83,0],[-0.31,-3.35],[-0.38,0],[-0.02,0],[0.04,0.42]],"o":[[-2.24,0],[0,0],[0,-0.41],[-0.42,0],[0,0],[0,0.41],[0,0],[0.42,0],[0,-0.41],[0,0],[1.21,-1.28],[3.39,0],[0.04,0.39],[0.02,0],[0.42,-0.04],[-0.38,-4.12]],"v":[[-0.307,-4.025],[-6.177,-1.475],[-6.177,-3.235],[-6.937,-3.985],[-7.687,-3.235],[-7.687,0.245],[-6.927,0.995],[-3.427,0.995],[-2.667,0.245],[-3.427,-0.505],[-5.027,-0.505],[-0.307,-2.525],[6.183,3.345],[6.933,4.025],[7.003,4.025],[7.683,3.205]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-18-autorenew').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Fill","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[250.307,246.025],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[250.307,246.025],"ix":2},"a":{"a":0,"k":[250.307,246.025],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":1,"st":-60,"ct":1,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250.002,250.002,0],"ix":2,"l":2},"a":{"a":0,"k":[250,250,0],"ix":1,"l":2},"s":{"a":0,"k":[2083,2083,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.41,0],[0,0],[0,-0.41],[-0.42,0],[0,0],[1.83,0],[0.31,3.35],[0.41,-0.05],[-0.04,-0.42],[-4.17,0],[-1.5,1.58],[0,0],[-0.42,0],[0,0.41],[0,0]],"o":[[0,0],[-0.42,0],[0,0.41],[0,0],[-1.22,1.28],[-3.39,0],[-0.04,-0.41],[-0.42,0.04],[0.38,4.12],[2.22,0],[0,0],[0,0.41],[0.42,0],[0,0],[-0.01,-0.42]],"v":[[6.917,-0.992],[3.417,-0.992],[2.657,-0.242],[3.417,0.508],[5.047,0.508],[0.317,2.528],[-6.173,-3.342],[-6.993,-4.022],[-7.673,-3.202],[0.317,4.028],[6.157,1.508],[6.157,3.238],[6.917,3.988],[7.677,3.238],[7.677,-0.242]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-18-autorenew').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Fill","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[249.683,253.972],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[4.17,0],[1.51,-1.59],[0,0],[0.42,0],[0,-0.41],[0,0],[-0.42,0],[0,0],[0,0.41],[0.42,0],[0,0],[-1.83,0],[-0.31,-3.35],[-0.38,0],[-0.02,0],[0.04,0.42]],"o":[[-2.24,0],[0,0],[0,-0.41],[-0.42,0],[0,0],[0,0.41],[0,0],[0.42,0],[0,-0.41],[0,0],[1.21,-1.28],[3.39,0],[0.04,0.39],[0.02,0],[0.42,-0.04],[-0.38,-4.12]],"v":[[-0.307,-4.025],[-6.177,-1.475],[-6.177,-3.235],[-6.937,-3.985],[-7.687,-3.235],[-7.687,0.245],[-6.927,0.995],[-3.427,0.995],[-2.667,0.245],[-3.427,-0.505],[-5.027,-0.505],[-0.307,-2.525],[6.183,3.345],[6.933,4.025],[7.003,4.025],[7.683,3.205]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-18-autorenew').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Fill","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[250.307,246.025],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[250.307,246.025],"ix":2},"a":{"a":0,"k":[250.307,246.025],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":60,"op":300,"st":0,"ct":1,"bm":0}]},{"id":"comp_2","nm":"loop-autorenew","fr":60,"layers":[{"ddd":0,"ind":1,"ty":3,"nm":"NULL ","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":60,"s":[-360]}],"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2,"l":2},"a":{"a":0,"k":[50,50,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".primary.design","cl":"primary design","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[56.365,-32.706,0],"ix":2,"l":2},"a":{"a":0,"k":[256.365,167.294,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[36.203,36.206],[-36.201,36.206],[-36.203,-36.206]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-18-autorenew').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[149.137,136.205],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[-54.792,0],[-7.083,-76.459]],"o":[[0,0],[26.458,-43.959],[78.542,0],[0,0]],"v":[[-139.584,5.208],[-139.584,5],[-10.208,-68.125],[139.584,68.125]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-18-autorenew').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[260.212,167.294],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":3,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":75,"st":1,"ct":1,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".primary.design","cl":"primary design","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[43.319,132.712,0],"ix":2,"l":2},"a":{"a":0,"k":[243.319,332.712,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-36.203,-36.206],[36.201,-36.206],[36.203,36.206]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-18-autorenew').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[350.223,363.801],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[54.792,0],[7.083,76.459]],"o":[[0,0],[-26.458,43.959],[-78.542,0],[0,0]],"v":[[139.584,-5.208],[139.584,-5],[10.208,68.125],[-139.584,-68.125]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-18-autorenew').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":3,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"tr","p":{"a":0,"k":[239.795,332.712],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":3,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":75,"st":1,"ct":1,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":3,"nm":"control","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"ef":[{"ty":5,"nm":"primary","np":3,"mn":"ADBE Color Control","ix":1,"en":1,"ef":[{"ty":2,"nm":"Color","mn":"ADBE Color Control-0001","ix":1,"v":{"a":0,"k":[1,1,1],"ix":1}}]}],"ip":0,"op":201,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":0,"nm":"in-autorenew","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2,"l":2},"a":{"a":0,"k":[250,250,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":500,"h":500,"ip":0,"op":70,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":0,"nm":"hover-autorenew","refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2,"l":2},"a":{"a":0,"k":[250,250,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":500,"h":500,"ip":70,"op":140,"st":70,"bm":0},{"ddd":0,"ind":4,"ty":0,"nm":"loop-autorenew","refId":"comp_2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2,"l":2},"a":{"a":0,"k":[250,250,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":500,"h":500,"ip":140,"op":210,"st":140,"bm":0}],"markers":[{"tm":0,"cm":"in-autorenew","dr":60},{"tm":70,"cm":"default:hover-autorenew","dr":60},{"tm":140,"cm":"loop-autorenew","dr":60}],"props":{}} \ No newline at end of file diff --git a/frontend/src/layouts/AppLayout/AppLayout.tsx b/frontend/src/layouts/AppLayout/AppLayout.tsx index aefe6ef62..44b3964bc 100644 --- a/frontend/src/layouts/AppLayout/AppLayout.tsx +++ b/frontend/src/layouts/AppLayout/AppLayout.tsx @@ -505,7 +505,7 @@ export const AppLayout = ({ children }: LayoutProps) => { } icon="rotation" > - Secret rotation + Secret Rotation
@@ -517,7 +517,7 @@ export const AppLayout = ({ children }: LayoutProps) => { } icon="system-outline-189-domain-verification" > - Secret approvals + Secret Approvals {Boolean(secretApprovalReqCount?.open) && ( {secretApprovalReqCount?.open} From 2de898fdbd891d32c3e477e10fc197b183960b81 Mon Sep 17 00:00:00 2001 From: Akhil Mohan Date: Mon, 30 Oct 2023 22:19:12 +0530 Subject: [PATCH 18/33] feat: backward compatiable enc key --- backend/src/ee/secretRotation/queue.ts | 34 +++++++++++++----- backend/src/ee/secretRotation/service.ts | 44 ++++++++++++++++++------ 2 files changed, 58 insertions(+), 20 deletions(-) diff --git a/backend/src/ee/secretRotation/queue.ts b/backend/src/ee/secretRotation/queue.ts index c545a9ac2..501e4d748 100644 --- a/backend/src/ee/secretRotation/queue.ts +++ b/backend/src/ee/secretRotation/queue.ts @@ -5,7 +5,7 @@ import { customAlphabet } from "nanoid"; import { Client as PgClient } from "pg"; import mysql from "mysql"; -import { client, getRootEncryptionKey } from "../../config"; +import { client, getEncryptionKey, getRootEncryptionKey } from "../../config"; import { BotService, EventService, TelemetryService } from "../../services"; import { SecretRotation } from "./models"; import { rotationTemplates } from "./templates"; @@ -21,9 +21,12 @@ import { TProviderFunction, TProviderFunctionTypes } from "./types"; -import { encryptSymmetric128BitHexKeyUTF8 } from "../../utils/crypto"; +import { + decryptSymmetric128BitHexKeyUTF8, + encryptSymmetric128BitHexKeyUTF8 +} from "../../utils/crypto"; import { ISecret, Secret } from "../../models"; -import { SECRET_SHARED } from "../../variables"; +import { ENCODING_SCHEME_BASE64, ENCODING_SCHEME_UTF8, SECRET_SHARED } from "../../variables"; import { EESecretService } from "../services"; import { SecretVersion } from "../models"; import { eventPushSecrets } from "../../events"; @@ -215,13 +218,26 @@ secretRotationQueue.process(async (job: Job) => { ) as ISecretRotationProviderTemplate; // decrypt user provided inputs for secret rotation + const encryptionKey = await getEncryptionKey(); const rootEncryptionKey = await getRootEncryptionKey(); - const decryptedData = client.decryptSymmetric( - secretRotation.encryptedData, - rootEncryptionKey, - secretRotation.encryptedDataIV, - secretRotation.encryptedDataTag - ); + let decryptedData = ""; + if (rootEncryptionKey && secretRotation.keyEncoding === ENCODING_SCHEME_BASE64) { + // case: encoding scheme is base64 + decryptedData = client.decryptSymmetric( + secretRotation.encryptedData, + rootEncryptionKey, + secretRotation.encryptedDataIV, + secretRotation.encryptedDataTag + ); + } else if (encryptionKey && secretRotation.keyEncoding === ENCODING_SCHEME_UTF8) { + // case: encoding scheme is utf8 + decryptedData = decryptSymmetric128BitHexKeyUTF8({ + ciphertext: secretRotation.encryptedData, + iv: secretRotation.encryptedDataIV, + tag: secretRotation.encryptedDataTag, + key: encryptionKey + }); + } const variables = JSON.parse(decryptedData) as ISecretRotationEncData; diff --git a/backend/src/ee/secretRotation/service.ts b/backend/src/ee/secretRotation/service.ts index 8476dcbb8..2ed68d646 100644 --- a/backend/src/ee/secretRotation/service.ts +++ b/backend/src/ee/secretRotation/service.ts @@ -1,10 +1,16 @@ import { ISecretRotationEncData, TCreateSecretRotation, TGetProviderTemplates } from "./types"; import { rotationTemplates } from "./templates"; import { SecretRotation } from "./models"; -import { client, getRootEncryptionKey } from "../../config"; +import { client, getEncryptionKey, getRootEncryptionKey } from "../../config"; import { BadRequestError } from "../../utils/errors"; import Ajv from "ajv"; import { removeSecretRotationQueue, startSecretRotationQueue } from "./queue"; +import { + ALGORITHM_AES_256_GCM, + ENCODING_SCHEME_BASE64, + ENCODING_SCHEME_UTF8 +} from "../../variables"; +import { encryptSymmetric128BitHexKeyUTF8 } from "../../utils/crypto"; const ajv = new Ajv(); @@ -51,24 +57,40 @@ export const createSecretRotation = async ({ creds: [] }; - const rootEncryptionKey = await getRootEncryptionKey(); - const { ciphertext, iv, tag } = client.encryptSymmetric( - JSON.stringify(encData), - rootEncryptionKey - ); - const secretRotation = new SecretRotation({ workspace: workspaceId, provider, environment, secretPath, interval, - outputs: Object.entries(outputs).map(([key, secret]) => ({ key, secret })), - encryptedData: ciphertext, - encryptedDataIV: iv, - encryptedDataTag: tag + outputs: Object.entries(outputs).map(([key, secret]) => ({ key, secret })) }); + const encryptionKey = await getEncryptionKey(); + const rootEncryptionKey = await getRootEncryptionKey(); + + if (rootEncryptionKey) { + const { ciphertext, iv, tag } = client.encryptSymmetric( + JSON.stringify(encData), + rootEncryptionKey + ); + secretRotation.encryptedDataIV = iv; + secretRotation.encryptedDataTag = tag; + secretRotation.encryptedData = ciphertext; + secretRotation.algorithm = ALGORITHM_AES_256_GCM; + secretRotation.keyEncoding = ENCODING_SCHEME_BASE64; + } else if (encryptionKey) { + const { ciphertext, iv, tag } = encryptSymmetric128BitHexKeyUTF8({ + plaintext: JSON.stringify(encData), + key: encryptionKey + }); + secretRotation.encryptedDataIV = iv; + secretRotation.encryptedDataTag = tag; + secretRotation.encryptedData = ciphertext; + secretRotation.algorithm = ALGORITHM_AES_256_GCM; + secretRotation.keyEncoding = ENCODING_SCHEME_UTF8; + } + await secretRotation.save(); await startSecretRotationQueue(secretRotation._id.toString(), interval); From c36352f05fc4c2e4ed3ae9d28a3c8810188b69f4 Mon Sep 17 00:00:00 2001 From: Akhil Mohan Date: Tue, 31 Oct 2023 15:44:05 +0530 Subject: [PATCH 19/33] feat(secret-rotation): updated helper text for options --- .../src/ee/secretRotation/templates/mysql.ts | 16 +++++++++--- .../ee/secretRotation/templates/postgres.ts | 16 +++++++++--- .../ee/secretRotation/templates/sendgrid.ts | 10 ++++--- backend/src/ee/secretRotation/types.ts | 2 +- .../components/v2/FormControl/FormControl.tsx | 14 +++++++--- .../steps/RotationInputForm.tsx | 26 ++++++++++++++++--- .../steps/RotationOutputForm.tsx | 16 ++++++++---- 7 files changed, 76 insertions(+), 24 deletions(-) diff --git a/backend/src/ee/secretRotation/templates/mysql.ts b/backend/src/ee/secretRotation/templates/mysql.ts index b57666596..357de6919 100644 --- a/backend/src/ee/secretRotation/templates/mysql.ts +++ b/backend/src/ee/secretRotation/templates/mysql.ts @@ -1,4 +1,4 @@ -import { TProviderFunctionTypes, TDbProviderClients, TAssignOp } from "../types"; +import { TAssignOp, TDbProviderClients, TProviderFunctionTypes } from "../types"; export const MYSQL_TEMPLATE = { inputs: { @@ -9,9 +9,17 @@ export const MYSQL_TEMPLATE = { host: { type: "string" as const }, database: { type: "string" as const }, port: { type: "integer" as const, default: "3306" }, - username1: { type: "string", default: "infisical-sql-user1" }, - username2: { type: "string", default: "infisical-sql-user2" }, - ca: { type: "string" } + username1: { + type: "string", + default: "infisical-sql-user1", + desc: "This user must be created in your database" + }, + username2: { + type: "string", + default: "infisical-sql-user2", + desc: "This user must be created in your database" + }, + ca: { type: "string", desc: "SSL certificate for db auth(string)" } }, required: [ "admin_username", diff --git a/backend/src/ee/secretRotation/templates/postgres.ts b/backend/src/ee/secretRotation/templates/postgres.ts index c4981cf29..3b3153be1 100644 --- a/backend/src/ee/secretRotation/templates/postgres.ts +++ b/backend/src/ee/secretRotation/templates/postgres.ts @@ -1,4 +1,4 @@ -import { TProviderFunctionTypes, TDbProviderClients, TAssignOp } from "../types"; +import { TAssignOp, TDbProviderClients, TProviderFunctionTypes } from "../types"; export const POSTGRES_TEMPLATE = { inputs: { @@ -9,9 +9,17 @@ export const POSTGRES_TEMPLATE = { host: { type: "string" as const }, database: { type: "string" as const }, port: { type: "integer" as const, default: "5432" }, - username1: { type: "string", default: "infisical-pg-user1" }, - username2: { type: "string", default: "infisical-pg-user2" }, - ca: { type: "string" } + username1: { + type: "string", + default: "infisical-pg-user1", + desc: "This user must be created in your database" + }, + username2: { + type: "string", + default: "infisical-pg-user2", + desc: "This user must be created in your database" + }, + ca: { type: "string", desc: "SSL certificate for db auth(string)" } }, required: [ "admin_username", diff --git a/backend/src/ee/secretRotation/templates/sendgrid.ts b/backend/src/ee/secretRotation/templates/sendgrid.ts index 454c9a145..ada62a0e6 100644 --- a/backend/src/ee/secretRotation/templates/sendgrid.ts +++ b/backend/src/ee/secretRotation/templates/sendgrid.ts @@ -1,11 +1,15 @@ -import { TProviderFunctionTypes, TAssignOp } from "../types"; +import { TAssignOp, TProviderFunctionTypes } from "../types"; export const SENDGRID_TEMPLATE = { inputs: { type: "object" as const, properties: { - admin_api_key: { type: "string" as const }, - scopes: { type: "array", items: { type: "string" as const } } + admin_api_key: { type: "string" as const, desc: "Sendgrid admin api key to create new keys" }, + scopes: { + type: "array", + items: { type: "string" as const }, + desc: "Scopes for created tokens by rotation(Array)" + } }, required: ["admin_api_key", "scopes"], additionalProperties: false diff --git a/backend/src/ee/secretRotation/types.ts b/backend/src/ee/secretRotation/types.ts index 4aa5975de..36ad36798 100644 --- a/backend/src/ee/secretRotation/types.ts +++ b/backend/src/ee/secretRotation/types.ts @@ -103,7 +103,7 @@ export type TProviderFunction = THttpProviderFunction | TDbProviderFunction; export type TProviderTemplate = { inputs: { type: "object"; - properties: Record; + properties: Record; required?: string[]; }; outputs: Record; diff --git a/frontend/src/components/v2/FormControl/FormControl.tsx b/frontend/src/components/v2/FormControl/FormControl.tsx index 91dfd4139..8f2e57f48 100644 --- a/frontend/src/components/v2/FormControl/FormControl.tsx +++ b/frontend/src/components/v2/FormControl/FormControl.tsx @@ -9,16 +9,24 @@ export type FormLabelProps = { isRequired?: boolean; label?: ReactNode; icon?: ReactNode; + className?: string; }; -export const FormLabel = ({ id, label, isRequired, icon }: FormLabelProps) => ( +export const FormLabel = ({ id, label, isRequired, icon, className }: FormLabelProps) => ( {label} {isRequired && *} - {icon && {icon}} + {icon && ( + + {icon} + + )} ); diff --git a/frontend/src/views/SecretRotationPage/components/CreateRotationForm/steps/RotationInputForm.tsx b/frontend/src/views/SecretRotationPage/components/CreateRotationForm/steps/RotationInputForm.tsx index 8c50687d1..125515bf1 100644 --- a/frontend/src/views/SecretRotationPage/components/CreateRotationForm/steps/RotationInputForm.tsx +++ b/frontend/src/views/SecretRotationPage/components/CreateRotationForm/steps/RotationInputForm.tsx @@ -1,14 +1,16 @@ import { Controller, useForm } from "react-hook-form"; +import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; -import { Button, FormControl, SecretInput } from "@app/components/v2"; +import { Button, FormControl, FormLabel, SecretInput, Tooltip } from "@app/components/v2"; type Props = { onSubmit: (data: Record) => void; onCancel: () => void; inputSchema: { - properties: Record; + properties: Record; required: string[]; }; }; @@ -35,8 +37,24 @@ export const RotationInputForm = ({ onSubmit, onCancel, inputSchema }: Props) => render={({ field }) => ( + + {Boolean(inputSchema.properties[inputName]?.desc) && ( + + + + )} +
+ } > {!isSecretsLoading && - secrets?.map(({ key, _id }) => ( - - {key} - - ))} + secrets + ?.filter( + ({ _id }) => + value === _id || !Object.values(selectedSecrets || {}).includes(_id) + ) + ?.map(({ key, _id }) => ( + + {key} + + ))} {isSecretsLoading && ( From 8b522a3fb525008b438dc85b19b139c9f2508d78 Mon Sep 17 00:00:00 2001 From: Akhil Mohan Date: Tue, 31 Oct 2023 16:49:59 +0530 Subject: [PATCH 20/33] feat(secret-rotation): updated docs for secret rotation --- .../ee/secretRotation/templates/sendgrid.ts | 6 +-- .../platform/secret-rotation/mysql.mdx | 37 +++++++++++++++++++ .../platform/secret-rotation/overview.mdx | 37 +++++++++++++++++++ .../platform/secret-rotation/postgres.mdx | 37 +++++++++++++++++++ .../platform/secret-rotation/sendgrid.mdx | 31 ++++++++++++++++ docs/mint.json | 14 ++++++- 6 files changed, 158 insertions(+), 4 deletions(-) create mode 100644 docs/documentation/platform/secret-rotation/mysql.mdx create mode 100644 docs/documentation/platform/secret-rotation/overview.mdx create mode 100644 docs/documentation/platform/secret-rotation/postgres.mdx create mode 100644 docs/documentation/platform/secret-rotation/sendgrid.mdx diff --git a/backend/src/ee/secretRotation/templates/sendgrid.ts b/backend/src/ee/secretRotation/templates/sendgrid.ts index ada62a0e6..b600f3e0c 100644 --- a/backend/src/ee/secretRotation/templates/sendgrid.ts +++ b/backend/src/ee/secretRotation/templates/sendgrid.ts @@ -5,13 +5,13 @@ export const SENDGRID_TEMPLATE = { type: "object" as const, properties: { admin_api_key: { type: "string" as const, desc: "Sendgrid admin api key to create new keys" }, - scopes: { + api_key_scopes: { type: "array", items: { type: "string" as const }, desc: "Scopes for created tokens by rotation(Array)" } }, - required: ["admin_api_key", "scopes"], + required: ["admin_api_key", "api_key_scopes"], additionalProperties: false }, outputs: { @@ -30,7 +30,7 @@ export const SENDGRID_TEMPLATE = { }, body: { name: "infisical-${random | 16}", - scopes: { ref: "inputs.scopes" } + scopes: { ref: "inputs.api_key_scopes" } }, setter: { "outputs.api_key": { diff --git a/docs/documentation/platform/secret-rotation/mysql.mdx b/docs/documentation/platform/secret-rotation/mysql.mdx new file mode 100644 index 000000000..b630e349a --- /dev/null +++ b/docs/documentation/platform/secret-rotation/mysql.mdx @@ -0,0 +1,37 @@ +--- +title: "MySQL/MariaDB" +description: "Rotated database user password of a MySQL or MariaDB" +--- + +Infisical will update periodically the provided database user's password. + + + At present Infisical do require access to your database. We will soon be released Infisical agent based rotation which would help you rotate without direct database access from Infisical cloud. + + +## Working + +1. User's has to create the two user's for Infisical to rotate and provide them required database access +2. Infisical will connect with your database with admin access +3. If last rotated one was username1, then username2 is chosen to be rotated +5. Update it's password with random value +6. After testing it gets saved to the provided secret mapping + +## Rotation Configuration + +1. Head over to Secret Rotation configuration page of your project by clicking on side bar `Secret Rotation` +2. Click on `MySQL` +3. Provide the inputs + - Admin Username: DB admin username + - Admin Password: DB admin password + - Host: DB host + - Port: DB port(number) + - Username1: The first username in two to rotate + - Username2: The second username in two to rotate + - CA: Certificate to connect with database(string) +4. Final step + - Select `Environment`, `Secret Path` and `Interval` to rotate the secrets + - Finally select the secrets in your provided board to replace with new secret after each rotation + - Your done and good to go. + +Congrats. You have 10x your MySQL/MariaDB access security. diff --git a/docs/documentation/platform/secret-rotation/overview.mdx b/docs/documentation/platform/secret-rotation/overview.mdx new file mode 100644 index 000000000..a11142106 --- /dev/null +++ b/docs/documentation/platform/secret-rotation/overview.mdx @@ -0,0 +1,37 @@ +--- +title: "Secret Rotation Overview" +description: "Keep your credentials safe by rotation" +--- + +Secret rotation is the process of periodically changing the values of secrets. This is done to reduce the risk of secrets being compromised and used to gain unauthorized access to systems or data. + +Rotated secrets can be +1. API key for an external service +2. Database credentials + +## How does the rotation happen? + +There are four phases in secret rotation and its triggered periodically in an internval. + +1. Creation + +System will create secret by calling an external service like an API call, or randomly generate a value. +Now there exist three valid secrets. + +2. Test + +Test the new secret key by some check to ensure its working one. Thus only two will be considered active and the other is considered inactive. + +3. Deletion + +System will remove the inactive secret and now there exist two valid secrets + +4. Finish + +System will switch the secret value from the rotated ones and trigger side effects like webhooks and events. + +## Infisical Secret Rotation Strategies + +1. [SendGrid](./sendgrid) +2. [PostgreSQL/CockroachDB](./postgres) +3. [MySQL/MariaDB](./mysql) diff --git a/docs/documentation/platform/secret-rotation/postgres.mdx b/docs/documentation/platform/secret-rotation/postgres.mdx new file mode 100644 index 000000000..167b60fcd --- /dev/null +++ b/docs/documentation/platform/secret-rotation/postgres.mdx @@ -0,0 +1,37 @@ +--- +title: "PostgreSQL/CockroachDB" +description: "Rotated database user password of a postgreSQL or cochroach db" +--- + +Infisical will update periodically the provided database user's password. + + + At present Infisical do require access to your database. We will soon be released Infisical agent based rotation which would help you rotate without direct database access from Infisical cloud. + + +## Working + +1. User's has to create the two user's for Infisical to rotate and provide them required database access +2. Infisical will connect with your database with admin access +3. If last rotated one was username1, then username2 is chosen to be rotated +5. Update it's password with random value +6. After testing it gets saved to the provided secret mapping + +## Rotation Configuration + +1. Head over to Secret Rotation configuration page of your project by clicking on side bar `Secret Rotation` +2. Click on `PostgreSQL` +3. Provide the inputs + - Admin Username: DB admin username + - Admin Password: DB admin password + - Host: DB host + - Port: DB port(number) + - Username1: The first username in two to rotate + - Username2: The second username in two to rotate + - CA: Certificate to connect with database(string) +4. Final step + - Select `Environment`, `Secret Path` and `Interval` to rotate the secrets + - Finally select the secrets in your provided board to replace with new secret after each rotation + - Your done and good to go. + +Congrats. You have 10x your PostgreSQL/CockroachDB access security. diff --git a/docs/documentation/platform/secret-rotation/sendgrid.mdx b/docs/documentation/platform/secret-rotation/sendgrid.mdx new file mode 100644 index 000000000..c4dd2797f --- /dev/null +++ b/docs/documentation/platform/secret-rotation/sendgrid.mdx @@ -0,0 +1,31 @@ +--- +title: "Twilio SendGrid" +description: "Rotate Twilio SendGrid API keys" +--- + +Twilio SendGrid is a cloud-based email delivery platform that helps businesses send transactional and marketing emails. +It uses an API key to do various operations. Using Infisical you can easily dynamically change the keys. + +## Working + +1. Infisical will need an admin token of SendGrid to create API keys dynamically. +2. Using the given admin token and scope by user Infisical will create and rotate API keys periodically +3. Under the hood infisical uses [SendGrid API](https://docs.sendgrid.com/api-reference/api-keys/create-api-keys) + +## Rotation Configuration + +1. Head over to Secret Rotation configuration page of your project by clicking on side bar `Secret Rotation` +2. Click on `Twilio SendGrid Card` +3. Provide the inputs + - Admin API Key: + SendGrid admin key to create lower scoped API keys. + - API Key Scopes + SendGrid generated API Key's scopes. For more info refer [this doc](https://docs.sendgrid.com/api-reference/api-key-permissions/api-key-permissions) + +4. Final step + - Select `Environment`, `Secret Path` and `Interval` to rotate the secrets + - Finally select the secrets in your provided board to replace with new secret after each rotation + - Your done and good to go. + +Now your output mapped secret value will be replaced periodically by SendGrid. + diff --git a/docs/mint.json b/docs/mint.json index 92876198d..05904b56b 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -34,7 +34,10 @@ } }, "topbarLinks": [ - { "name": "Log In", "url": "https://app.infisical.com/login" } + { + "name": "Log In", + "url": "https://app.infisical.com/login" + } ], "topbarCtaButton": { "name": "Start for Free", @@ -120,6 +123,15 @@ "documentation/platform/audit-logs", "documentation/platform/token", "documentation/platform/mfa", + { + "group": "Secret Rotation", + "pages": [ + "documentation/platform/secret-rotation/overview", + "documentation/platform/secret-rotation/sendgrid", + "documentation/platform/secret-rotation/postgres", + "documentation/platform/secret-rotation/mysql" + ] + }, { "group": "SSO", "pages": [ From c1ea441e3a6628d4ec5279f7b666852beb1b9a75 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Wed, 1 Nov 2023 01:08:15 -0400 Subject: [PATCH 21/33] AJV set strict to false --- backend/src/ee/secretRotation/queue.ts | 2 +- backend/src/ee/secretRotation/service.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/ee/secretRotation/queue.ts b/backend/src/ee/secretRotation/queue.ts index 501e4d748..132627221 100644 --- a/backend/src/ee/secretRotation/queue.ts +++ b/backend/src/ee/secretRotation/queue.ts @@ -189,7 +189,7 @@ const secretRotationRemoveFn = async (func: TProviderFunction, variables: ISecre secretRotationQueue.process(async (job: Job) => { const rotationStratDocId = job.data.rotationDocId; const secretRotation = await SecretRotation.findById(rotationStratDocId) - .select("+encryptedData +encryptedDataTag +encryptedDataIV") + .select("+encryptedData +encryptedDataTag +encryptedDataIV +keyEncoding") .populate<{ outputs: [ { diff --git a/backend/src/ee/secretRotation/service.ts b/backend/src/ee/secretRotation/service.ts index 2ed68d646..3770c520c 100644 --- a/backend/src/ee/secretRotation/service.ts +++ b/backend/src/ee/secretRotation/service.ts @@ -12,7 +12,7 @@ import { } from "../../variables"; import { encryptSymmetric128BitHexKeyUTF8 } from "../../utils/crypto"; -const ajv = new Ajv(); +const ajv = new Ajv({ strict: false }); export const getProviderTemplate = async ({ workspaceId }: TGetProviderTemplates) => { return { From 75eeda4278d8183659f28eeb3a6ddc14f9460916 Mon Sep 17 00:00:00 2001 From: Akhil Mohan Date: Wed, 1 Nov 2023 19:49:49 +0530 Subject: [PATCH 22/33] feat(secret-rotation): changed to mysql2 client and refactored queue util functions --- backend/package-lock.json | 236 +++++++++++------- backend/package.json | 3 +- .../ee/secretRotation/{ => queue}/queue.ts | 192 ++------------ .../ee/secretRotation/queue/queue.utils.ts | 179 +++++++++++++ backend/src/ee/secretRotation/service.ts | 2 +- .../src/ee/secretRotation/templates/mysql.ts | 3 +- 6 files changed, 342 insertions(+), 273 deletions(-) rename backend/src/ee/secretRotation/{ => queue}/queue.ts (59%) create mode 100644 backend/src/ee/secretRotation/queue/queue.utils.ts diff --git a/backend/package-lock.json b/backend/package-lock.json index bf0abb3ed..28b45c4df 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -46,7 +46,7 @@ "libsodium-wrappers": "^0.7.10", "lodash": "^4.17.21", "mongoose": "^7.4.1", - "mysql": "^2.18.1", + "mysql2": "^3.6.2", "nanoid": "^3.3.6", "node-cache": "^5.1.2", "nodemailer": "^6.8.0", @@ -82,7 +82,6 @@ "@types/jmespath": "^0.15.1", "@types/jsonwebtoken": "^8.5.9", "@types/lodash": "^4.14.191", - "@types/mysql": "^2.15.23", "@types/node": "^18.11.3", "@types/nodemailer": "^6.4.6", "@types/passport": "^1.0.12", @@ -5925,15 +5924,6 @@ "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz", "integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==" }, - "node_modules/@types/mysql": { - "version": "2.15.23", - "resolved": "https://registry.npmjs.org/@types/mysql/-/mysql-2.15.23.tgz", - "integrity": "sha512-l3mEJpU1VNkt7uJP2vYWZrfjxlzQtwJNKSWe+7sgChuDjFoyRfu263f9pBDlhFPmyda23o8GNGq6FL5CHSd/QA==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/node": { "version": "18.16.19", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.19.tgz", @@ -7011,14 +7001,6 @@ "@juanelas/base64": "^1.1.2" } }, - "node_modules/bignumber.js": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.0.tgz", - "integrity": "sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A==", - "engines": { - "node": "*" - } - }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -8760,6 +8742,14 @@ "node": ">=10" } }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "dependencies": { + "is-property": "^1.0.2" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -9435,6 +9425,11 @@ "node": ">=0.10.0" } }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==" + }, "node_modules/is-retry-allowed": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-2.2.0.tgz", @@ -10861,45 +10856,76 @@ "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.2" } }, - "node_modules/mysql": { - "version": "2.18.1", - "resolved": "https://registry.npmjs.org/mysql/-/mysql-2.18.1.tgz", - "integrity": "sha512-Bca+gk2YWmqp2Uf6k5NFEurwY/0td0cpebAucFpY/3jhrwrVGuxU2uQFCHjU19SJfje0yQvi+rVWdq78hR5lig==", + "node_modules/mysql2": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.6.2.tgz", + "integrity": "sha512-m5erE6bMoWfPXW1D5UrVwlT8PowAoSX69KcZzPuARQ3wY1RJ52NW9PdvdPo076XiSIkQ5IBTis7hxdlrQTlyug==", "dependencies": { - "bignumber.js": "9.0.0", - "readable-stream": "2.3.7", - "safe-buffer": "5.1.2", - "sqlstring": "2.3.1" + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.6.3", + "long": "^5.2.1", + "lru-cache": "^8.0.0", + "named-placeholders": "^1.1.3", + "seq-queue": "^0.0.5", + "sqlstring": "^2.3.2" }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/mysql2/node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/mysql2/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/mysql2/node_modules/lru-cache": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-8.0.5.tgz", + "integrity": "sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA==", + "engines": { + "node": ">=16.14" + } + }, + "node_modules/mysql2/node_modules/sqlstring": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", + "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", "engines": { "node": ">= 0.6" } }, - "node_modules/mysql/node_modules/readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "node_modules/named-placeholders": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.3.tgz", + "integrity": "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==", "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" + "lru-cache": "^7.14.1" + }, + "engines": { + "node": ">=12.0.0" } }, - "node_modules/mysql/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "node_modules/mysql/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dependencies": { - "safe-buffer": "~5.1.0" + "node_modules/named-placeholders/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "engines": { + "node": ">=12" } }, "node_modules/nanoid": { @@ -15588,6 +15614,11 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, + "node_modules/seq-queue": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", + "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" + }, "node_modules/serve-static": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", @@ -21688,15 +21719,6 @@ "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz", "integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==" }, - "@types/mysql": { - "version": "2.15.23", - "resolved": "https://registry.npmjs.org/@types/mysql/-/mysql-2.15.23.tgz", - "integrity": "sha512-l3mEJpU1VNkt7uJP2vYWZrfjxlzQtwJNKSWe+7sgChuDjFoyRfu263f9pBDlhFPmyda23o8GNGq6FL5CHSd/QA==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, "@types/node": { "version": "18.16.19", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.19.tgz", @@ -22528,11 +22550,6 @@ "@juanelas/base64": "^1.1.2" } }, - "bignumber.js": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.0.tgz", - "integrity": "sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A==" - }, "binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -23830,6 +23847,14 @@ "wide-align": "^1.1.2" } }, + "generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "requires": { + "is-property": "^1.0.2" + } + }, "gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -24306,6 +24331,11 @@ "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==" }, + "is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==" + }, "is-retry-allowed": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-2.2.0.tgz", @@ -25391,43 +25421,58 @@ "node-gyp-build-optional-packages": "5.0.7" } }, - "mysql": { - "version": "2.18.1", - "resolved": "https://registry.npmjs.org/mysql/-/mysql-2.18.1.tgz", - "integrity": "sha512-Bca+gk2YWmqp2Uf6k5NFEurwY/0td0cpebAucFpY/3jhrwrVGuxU2uQFCHjU19SJfje0yQvi+rVWdq78hR5lig==", + "mysql2": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.6.2.tgz", + "integrity": "sha512-m5erE6bMoWfPXW1D5UrVwlT8PowAoSX69KcZzPuARQ3wY1RJ52NW9PdvdPo076XiSIkQ5IBTis7hxdlrQTlyug==", "requires": { - "bignumber.js": "9.0.0", - "readable-stream": "2.3.7", - "safe-buffer": "5.1.2", - "sqlstring": "2.3.1" + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.6.3", + "long": "^5.2.1", + "lru-cache": "^8.0.0", + "named-placeholders": "^1.1.3", + "seq-queue": "^0.0.5", + "sqlstring": "^2.3.2" }, "dependencies": { - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==" + }, + "iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" + "safer-buffer": ">= 2.1.2 < 3.0.0" } }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + "lru-cache": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-8.0.5.tgz", + "integrity": "sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA==" }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } + "sqlstring": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", + "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==" + } + } + }, + "named-placeholders": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.3.tgz", + "integrity": "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==", + "requires": { + "lru-cache": "^7.14.1" + }, + "dependencies": { + "lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==" } } }, @@ -28839,6 +28884,11 @@ } } }, + "seq-queue": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", + "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" + }, "serve-static": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", diff --git a/backend/package.json b/backend/package.json index 8d8f0228c..9768762e8 100644 --- a/backend/package.json +++ b/backend/package.json @@ -37,7 +37,7 @@ "libsodium-wrappers": "^0.7.10", "lodash": "^4.17.21", "mongoose": "^7.4.1", - "mysql": "^2.18.1", + "mysql2": "^3.6.2", "nanoid": "^3.3.6", "node-cache": "^5.1.2", "nodemailer": "^6.8.0", @@ -105,7 +105,6 @@ "@types/jmespath": "^0.15.1", "@types/jsonwebtoken": "^8.5.9", "@types/lodash": "^4.14.191", - "@types/mysql": "^2.15.23", "@types/node": "^18.11.3", "@types/nodemailer": "^6.4.6", "@types/passport": "^1.0.12", diff --git a/backend/src/ee/secretRotation/queue.ts b/backend/src/ee/secretRotation/queue/queue.ts similarity index 59% rename from backend/src/ee/secretRotation/queue.ts rename to backend/src/ee/secretRotation/queue/queue.ts index 132627221..0a29c7a6c 100644 --- a/backend/src/ee/secretRotation/queue.ts +++ b/backend/src/ee/secretRotation/queue/queue.ts @@ -1,191 +1,33 @@ -import axios from "axios"; import Queue, { Job } from "bull"; -import jmespath from "jmespath"; -import { customAlphabet } from "nanoid"; -import { Client as PgClient } from "pg"; -import mysql from "mysql"; - -import { client, getEncryptionKey, getRootEncryptionKey } from "../../config"; -import { BotService, EventService, TelemetryService } from "../../services"; -import { SecretRotation } from "./models"; -import { rotationTemplates } from "./templates"; +import { client, getEncryptionKey, getRootEncryptionKey } from "../../../config"; +import { BotService, EventService, TelemetryService } from "../../../services"; +import { SecretRotation } from "../models"; +import { rotationTemplates } from "../templates"; import { ISecretRotationData, ISecretRotationEncData, ISecretRotationProviderTemplate, - TAssignOp, - TDbProviderClients, - TDbProviderFunction, - TDirectAssignOp, - THttpProviderFunction, - TProviderFunction, TProviderFunctionTypes -} from "./types"; +} from "../types"; import { decryptSymmetric128BitHexKeyUTF8, encryptSymmetric128BitHexKeyUTF8 -} from "../../utils/crypto"; -import { ISecret, Secret } from "../../models"; -import { ENCODING_SCHEME_BASE64, ENCODING_SCHEME_UTF8, SECRET_SHARED } from "../../variables"; -import { EESecretService } from "../services"; -import { SecretVersion } from "../models"; -import { eventPushSecrets } from "../../events"; +} from "../../../utils/crypto"; +import { ISecret, Secret } from "../../../models"; +import { ENCODING_SCHEME_BASE64, ENCODING_SCHEME_UTF8, SECRET_SHARED } from "../../../variables"; +import { EESecretService } from "../../services"; +import { SecretVersion } from "../../models"; +import { eventPushSecrets } from "../../../events"; -const REGEX = /\${([^}]+)}/g; -const SLUG_ALPHABETS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; -const nanoId = customAlphabet(SLUG_ALPHABETS, 10); +import { + secretRotationPreSetFn, + secretRotationRemoveFn, + secretRotationSetFn, + secretRotationTestFn +} from "./queue.utils"; const secretRotationQueue = new Queue("secret-rotation-service", process.env.REDIS_URL as string); -const interpolate = (data: any, getValue: (key: string) => unknown) => { - if (!data) return; - - if (typeof data === "number") return data; - - if (typeof data === "string") { - return data.replace(REGEX, (_a, b) => getValue(b) as string); - } - - if (typeof data === "object" && Array.isArray(data)) { - data.forEach((el, index) => { - data[index] = interpolate(el, getValue); - }); - } - - if (typeof data === "object") { - if ((data as { ref: string })?.ref) return getValue((data as { ref: string }).ref); - const temp = data as Record; // for converting ts object to record type - Object.keys(temp).forEach((key) => { - temp[key as keyof typeof temp] = interpolate(data[key as keyof typeof temp], getValue); - }); - } - return data; -}; - -const getInterpolationValue = (variables: ISecretRotationData) => (key: string) => { - if (key.includes("|")) { - const [keyword, ...arg] = key.split("|").map((el) => el.trim()); - switch (keyword) { - case "random": { - return nanoId(parseInt(arg[0], 10)); - } - default: { - throw Error(`Interpolation key not found - ${key}`); - } - } - } - const [type, keyName] = key.split(".").map((el) => el.trim()); - return variables[type as keyof ISecretRotationData][keyName]; -}; - -const secretRotationHttpFn = async ( - func: THttpProviderFunction, - variables: ISecretRotationData -) => { - // string interpolation - const headers = interpolate(func.header, getInterpolationValue(variables)); - const url = interpolate(func.url, getInterpolationValue(variables)); - const body = interpolate(func.body, getInterpolationValue(variables)); - // axios will automatically throw error if req status is not between 2xx range - return axios({ method: func.method, url, headers, data: body }); -}; - -const secretRotationDbFn = async (func: TDbProviderFunction, variables: ISecretRotationData) => { - const { type, client, pre, ...dbConnection } = func; - const { username, password, host, database, port, query, ca } = interpolate( - dbConnection, - getInterpolationValue(variables) - ); - const ssl = ca ? { rejectUnauthorized: false, ca } : undefined; - if (host === "localhost" || host === "127.0.0.1") throw new Error("Invalid db host"); - if (client === TDbProviderClients.Pg) { - const pgClient = new PgClient({ user: username, password, host, database, port, ssl }); - await pgClient.connect(); - const res = await pgClient.query(query); - await pgClient.end(); - return res.rows[0]; - } else if (client === TDbProviderClients.Sql) { - const sqlClient = mysql.createPool({ - user: username, - password, - host, - database, - port, - connectionLimit: 1, - ssl - }); - const res = await new Promise((resolve, reject) => { - sqlClient.query(query, (err, data) => { - if (err) return reject(err); - resolve(data); - }); - }); - await new Promise((resolve, reject) => { - sqlClient.end(function (err) { - if (err) return reject(err); - return resolve({}); - }); - }); - return (res as any)?.[0]; - } -}; - -const secretRotationPreSetFn = ( - op: Record, - variables: ISecretRotationData -) => { - const getValFn = getInterpolationValue(variables); - Object.entries(op || {}).forEach(([key, assignFn]) => { - const [type, keyName] = key.split(".") as [keyof ISecretRotationData, string]; - variables[type][keyName] = interpolate(assignFn.value, getValFn); - }); -}; - -const secretRotationSetFn = async (func: TProviderFunction, variables: ISecretRotationData) => { - const getValFn = getInterpolationValue(variables); - // http setter - if (func.type === TProviderFunctionTypes.HTTP) { - const res = await secretRotationHttpFn(func, variables); - Object.entries(func.setter || {}).forEach(([key, assignFn]) => { - const [type, keyName] = key.split(".") as [keyof ISecretRotationData, string]; - if (assignFn.assign === TAssignOp.JmesPath) { - variables[type][keyName] = jmespath.search(res.data, assignFn.path); - } else if (assignFn.value) { - variables[type][keyName] = interpolate(assignFn.value, getValFn); - } - }); - // db setter - } else if (func.type === TProviderFunctionTypes.DB) { - const data = await secretRotationDbFn(func, variables); - Object.entries(func.setter || {}).forEach(([key, assignFn]) => { - const [type, keyName] = key.split(".") as [keyof ISecretRotationData, string]; - if (assignFn.assign === TAssignOp.JmesPath) { - if (typeof data === "object") { - variables[type][keyName] = jmespath.search(data, assignFn.path); - } - } else if (assignFn.value) { - variables[type][keyName] = interpolate(assignFn.value, getValFn); - } - }); - } -}; - -const secretRotationTestFn = async (func: TProviderFunction, variables: ISecretRotationData) => { - if (func.type === TProviderFunctionTypes.HTTP) { - await secretRotationHttpFn(func, variables); - } else if (func.type === TProviderFunctionTypes.DB) { - await secretRotationDbFn(func, variables); - } -}; - -const secretRotationRemoveFn = async (func: TProviderFunction, variables: ISecretRotationData) => { - if (!func) return; - if (func.type === TProviderFunctionTypes.HTTP) { - // string interpolation - return await secretRotationHttpFn(func, variables); - } -}; - secretRotationQueue.process(async (job: Job) => { const rotationStratDocId = job.data.rotationDocId; const secretRotation = await SecretRotation.findById(rotationStratDocId) diff --git a/backend/src/ee/secretRotation/queue/queue.utils.ts b/backend/src/ee/secretRotation/queue/queue.utils.ts new file mode 100644 index 000000000..c1ddbefc1 --- /dev/null +++ b/backend/src/ee/secretRotation/queue/queue.utils.ts @@ -0,0 +1,179 @@ +import axios from "axios"; +import jmespath from "jmespath"; +import { customAlphabet } from "nanoid"; +import { Client as PgClient } from "pg"; +import mysql from "mysql2"; +import { + ISecretRotationData, + TAssignOp, + TDbProviderClients, + TDbProviderFunction, + TDirectAssignOp, + THttpProviderFunction, + TProviderFunction, + TProviderFunctionTypes +} from "../types"; +const REGEX = /\${([^}]+)}/g; +const SLUG_ALPHABETS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; +const nanoId = customAlphabet(SLUG_ALPHABETS, 10); + +export const interpolate = (data: any, getValue: (key: string) => unknown) => { + if (!data) return; + + if (typeof data === "number") return data; + + if (typeof data === "string") { + return data.replace(REGEX, (_a, b) => getValue(b) as string); + } + + if (typeof data === "object" && Array.isArray(data)) { + data.forEach((el, index) => { + data[index] = interpolate(el, getValue); + }); + } + + if (typeof data === "object") { + if ((data as { ref: string })?.ref) return getValue((data as { ref: string }).ref); + const temp = data as Record; // for converting ts object to record type + Object.keys(temp).forEach((key) => { + temp[key as keyof typeof temp] = interpolate(data[key as keyof typeof temp], getValue); + }); + } + return data; +}; + +const getInterpolationValue = (variables: ISecretRotationData) => (key: string) => { + if (key.includes("|")) { + const [keyword, ...arg] = key.split("|").map((el) => el.trim()); + switch (keyword) { + case "random": { + return nanoId(parseInt(arg[0], 10)); + } + default: { + throw Error(`Interpolation key not found - ${key}`); + } + } + } + const [type, keyName] = key.split(".").map((el) => el.trim()); + return variables[type as keyof ISecretRotationData][keyName]; +}; + +export const secretRotationHttpFn = async ( + func: THttpProviderFunction, + variables: ISecretRotationData +) => { + // string interpolation + const headers = interpolate(func.header, getInterpolationValue(variables)); + const url = interpolate(func.url, getInterpolationValue(variables)); + const body = interpolate(func.body, getInterpolationValue(variables)); + // axios will automatically throw error if req status is not between 2xx range + return axios({ method: func.method, url, headers, data: body }); +}; + +export const secretRotationDbFn = async ( + func: TDbProviderFunction, + variables: ISecretRotationData +) => { + const { type, client, pre, ...dbConnection } = func; + const { username, password, host, database, port, query, ca } = interpolate( + dbConnection, + getInterpolationValue(variables) + ); + const ssl = ca ? { rejectUnauthorized: false, ca } : undefined; + if (host === "localhost" || host === "127.0.0.1") throw new Error("Invalid db host"); + if (client === TDbProviderClients.Pg) { + const pgClient = new PgClient({ user: username, password, host, database, port, ssl }); + await pgClient.connect(); + const res = await pgClient.query(query); + await pgClient.end(); + return res.rows[0]; + } else if (client === TDbProviderClients.Sql) { + const sqlClient = mysql.createPool({ + user: username, + password, + host, + database, + port, + connectionLimit: 1, + ssl + }); + const res = await new Promise((resolve, reject) => { + sqlClient.query(query, (err, data) => { + if (err) return reject(err); + resolve(data); + }); + }); + await new Promise((resolve, reject) => { + sqlClient.end(function (err) { + if (err) return reject(err); + return resolve({}); + }); + }); + return (res as any)?.[0]; + } +}; + +export const secretRotationPreSetFn = ( + op: Record, + variables: ISecretRotationData +) => { + const getValFn = getInterpolationValue(variables); + Object.entries(op || {}).forEach(([key, assignFn]) => { + const [type, keyName] = key.split(".") as [keyof ISecretRotationData, string]; + variables[type][keyName] = interpolate(assignFn.value, getValFn); + }); +}; + +export const secretRotationSetFn = async ( + func: TProviderFunction, + variables: ISecretRotationData +) => { + const getValFn = getInterpolationValue(variables); + // http setter + if (func.type === TProviderFunctionTypes.HTTP) { + const res = await secretRotationHttpFn(func, variables); + Object.entries(func.setter || {}).forEach(([key, assignFn]) => { + const [type, keyName] = key.split(".") as [keyof ISecretRotationData, string]; + if (assignFn.assign === TAssignOp.JmesPath) { + variables[type][keyName] = jmespath.search(res.data, assignFn.path); + } else if (assignFn.value) { + variables[type][keyName] = interpolate(assignFn.value, getValFn); + } + }); + // db setter + } else if (func.type === TProviderFunctionTypes.DB) { + const data = await secretRotationDbFn(func, variables); + Object.entries(func.setter || {}).forEach(([key, assignFn]) => { + const [type, keyName] = key.split(".") as [keyof ISecretRotationData, string]; + if (assignFn.assign === TAssignOp.JmesPath) { + if (typeof data === "object") { + variables[type][keyName] = jmespath.search(data, assignFn.path); + } + } else if (assignFn.value) { + variables[type][keyName] = interpolate(assignFn.value, getValFn); + } + }); + } +}; + +export const secretRotationTestFn = async ( + func: TProviderFunction, + variables: ISecretRotationData +) => { + if (func.type === TProviderFunctionTypes.HTTP) { + await secretRotationHttpFn(func, variables); + } else if (func.type === TProviderFunctionTypes.DB) { + await secretRotationDbFn(func, variables); + } +}; + +export const secretRotationRemoveFn = async ( + func: TProviderFunction, + variables: ISecretRotationData +) => { + if (!func) return; + if (func.type === TProviderFunctionTypes.HTTP) { + // string interpolation + return await secretRotationHttpFn(func, variables); + } +}; diff --git a/backend/src/ee/secretRotation/service.ts b/backend/src/ee/secretRotation/service.ts index 3770c520c..9e00f20e1 100644 --- a/backend/src/ee/secretRotation/service.ts +++ b/backend/src/ee/secretRotation/service.ts @@ -4,7 +4,7 @@ import { SecretRotation } from "./models"; import { client, getEncryptionKey, getRootEncryptionKey } from "../../config"; import { BadRequestError } from "../../utils/errors"; import Ajv from "ajv"; -import { removeSecretRotationQueue, startSecretRotationQueue } from "./queue"; +import { removeSecretRotationQueue, startSecretRotationQueue } from "./queue/queue"; import { ALGORITHM_AES_256_GCM, ENCODING_SCHEME_BASE64, diff --git a/backend/src/ee/secretRotation/templates/mysql.ts b/backend/src/ee/secretRotation/templates/mysql.ts index 357de6919..ce44c753f 100644 --- a/backend/src/ee/secretRotation/templates/mysql.ts +++ b/backend/src/ee/secretRotation/templates/mysql.ts @@ -50,8 +50,7 @@ export const MYSQL_TEMPLATE = { database: "${inputs.database}", port: "${inputs.port}", ca: "${inputs.ca}", - query: - "ALTER USER ${internal.username} IDENTIFIED WITH mysql_native_password BY '${internal.rotated_password}'", + query: "ALTER USER ${internal.username} IDENTIFIED BY '${internal.rotated_password}'", setter: { "outputs.db_username": { assign: TAssignOp.Direct as const, From 40238788e5d30a1bb21f534898086f61dfb590ac Mon Sep 17 00:00:00 2001 From: Akhil Mohan Date: Wed, 1 Nov 2023 22:24:58 +0530 Subject: [PATCH 23/33] feat(secret-rotation): changed queue logging to pino --- backend/package-lock.json | 39 +++++++------------- backend/src/ee/secretRotation/queue/queue.ts | 3 +- 2 files changed, 16 insertions(+), 26 deletions(-) diff --git a/backend/package-lock.json b/backend/package-lock.json index 28b45c4df..c743979e6 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -7559,7 +7559,8 @@ "node_modules/core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==" + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "dev": true }, "node_modules/cors": { "version": "2.8.5", @@ -10409,6 +10410,11 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/long": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", + "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==" + }, "node_modules/lru_map": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/lru_map/-/lru_map-0.3.3.tgz", @@ -15067,11 +15073,6 @@ "node": ">= 0.6.0" } }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" - }, "node_modules/process-warning": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-2.3.0.tgz", @@ -15873,14 +15874,6 @@ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" }, - "node_modules/sqlstring": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.1.tgz", - "integrity": "sha512-ooAzh/7dxIG5+uDik1z/Rd1vli0+38izZhGzSa34FwR7IbelPWCCKSNIl8jlL/F7ERvy8CB2jNeM1E9i9mXMAQ==", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/stack-utils": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", @@ -22959,7 +22952,8 @@ "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==" + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "dev": true }, "cors": { "version": "2.8.5", @@ -25097,6 +25091,11 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "long": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", + "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==" + }, "lru_map": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/lru_map/-/lru_map-0.3.3.tgz", @@ -28506,11 +28505,6 @@ "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==" }, - "process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" - }, "process-warning": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-2.3.0.tgz", @@ -29093,11 +29087,6 @@ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" }, - "sqlstring": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.1.tgz", - "integrity": "sha512-ooAzh/7dxIG5+uDik1z/Rd1vli0+38izZhGzSa34FwR7IbelPWCCKSNIl8jlL/F7ERvy8CB2jNeM1E9i9mXMAQ==" - }, "stack-utils": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", diff --git a/backend/src/ee/secretRotation/queue/queue.ts b/backend/src/ee/secretRotation/queue/queue.ts index 0a29c7a6c..ff63d32c7 100644 --- a/backend/src/ee/secretRotation/queue/queue.ts +++ b/backend/src/ee/secretRotation/queue/queue.ts @@ -18,6 +18,7 @@ import { ENCODING_SCHEME_BASE64, ENCODING_SCHEME_UTF8, SECRET_SHARED } from "../ import { EESecretService } from "../../services"; import { SecretVersion } from "../../models"; import { eventPushSecrets } from "../../../events"; +import { logger } from "../../../utils/logging"; import { secretRotationPreSetFn, @@ -260,7 +261,7 @@ secretRotationQueue.process(async (job: Job) => { }); } } catch (err) { - console.error(err); + logger.error(err); await SecretRotation.findByIdAndUpdate(rotationStratDocId, { status: "failed", statusMessage: (err as Error).message, From a07bd5ad40b0c6876877ac4a05f6b6a6f39a34ba Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Wed, 1 Nov 2023 16:49:31 -0400 Subject: [PATCH 24/33] add log to secretRotationQueue process --- backend/src/ee/secretRotation/queue/queue.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/src/ee/secretRotation/queue/queue.ts b/backend/src/ee/secretRotation/queue/queue.ts index ff63d32c7..0127bbd9c 100644 --- a/backend/src/ee/secretRotation/queue/queue.ts +++ b/backend/src/ee/secretRotation/queue/queue.ts @@ -30,6 +30,7 @@ import { const secretRotationQueue = new Queue("secret-rotation-service", process.env.REDIS_URL as string); secretRotationQueue.process(async (job: Job) => { + logger.info(`secretRotationQueue.process: [rotationDocument=${job.data.rotationDocId}]`); const rotationStratDocId = job.data.rotationDocId; const secretRotation = await SecretRotation.findById(rotationStratDocId) .select("+encryptedData +encryptedDataTag +encryptedDataIV +keyEncoding") From 079a09a3d1c364833a0e8f37ae8ba65b5e846b1b Mon Sep 17 00:00:00 2001 From: Vladyslav Matsiiako Date: Wed, 1 Nov 2023 14:27:46 -0700 Subject: [PATCH 25/33] Remove create new org --- frontend/src/layouts/AppLayout/AppLayout.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/layouts/AppLayout/AppLayout.tsx b/frontend/src/layouts/AppLayout/AppLayout.tsx index 44b3964bc..632449273 100644 --- a/frontend/src/layouts/AppLayout/AppLayout.tsx +++ b/frontend/src/layouts/AppLayout/AppLayout.tsx @@ -319,7 +319,7 @@ export const AppLayout = ({ children }: LayoutProps) => { ))} - + {/*
- + */}
-
-
-
Pro Tips
- After creating an integration, your secrets will start syncing immediately. This might cause an unexpected override of current secrets in Checkly with secrets from Infisical. - If you have multiple Checkly integrations and are using suffixes for at least one of them, you will have to add suffixes for all the active Checkly integrations – otherwise you might run into rare unexpected behavior. -
) : (
diff --git a/frontend/src/views/IntegrationsPage/components/IntegrationsSection/IntegrationsSection.tsx b/frontend/src/views/IntegrationsPage/components/IntegrationsSection/IntegrationsSection.tsx index 9e23e7909..34d28ec14 100644 --- a/frontend/src/views/IntegrationsPage/components/IntegrationsSection/IntegrationsSection.tsx +++ b/frontend/src/views/IntegrationsPage/components/IntegrationsSection/IntegrationsSection.tsx @@ -163,12 +163,22 @@ export const IntegrationsSection = ({
)} {((integration.integration === "checkly") || (integration.integration === "github")) && ( -
- -
- {integration?.metadata?.secretSuffix || "-"} + <> + {integration.targetService && ( +
+ +
+ {integration.targetService} +
+
+ )} +
+ +
+ {integration?.metadata?.secretSuffix || "-"} +
-
+ )}
From abbeb67b95d1e91290fdf6affeff7eb04c1b5196 Mon Sep 17 00:00:00 2001 From: Akhil Mohan Date: Thu, 2 Nov 2023 14:39:13 +0530 Subject: [PATCH 28/33] fix: resolved error in org settings and integrations page alert hidden on no integrations --- .../src/ee/controllers/v1/roleController.ts | 12 +- .../permissions/OrgPermissionCan.tsx | 2 +- .../OrgPermissionContext.tsx | 6 +- .../src/hoc/withPermission/withPermission.tsx | 2 +- frontend/src/hooks/api/roles/queries.tsx | 18 ++- .../IntegrationsSection.tsx | 7 +- .../OrgDeleteSection/OrgDeleteSection.tsx | 127 ++++++++---------- .../OrgGeneralTab/OrgGeneralTab.tsx | 13 +- .../OrgIncidentContactsSection.tsx | 2 +- 9 files changed, 91 insertions(+), 98 deletions(-) diff --git a/backend/src/ee/controllers/v1/roleController.ts b/backend/src/ee/controllers/v1/roleController.ts index d80ef2817..edc31273a 100644 --- a/backend/src/ee/controllers/v1/roleController.ts +++ b/backend/src/ee/controllers/v1/roleController.ts @@ -212,12 +212,13 @@ export const getUserPermissions = async (req: Request, res: Response) => { const { params: { orgId } } = await validateRequest(GetUserPermission, req); - - const { permission } = await getUserOrgPermissions(req.user._id, orgId); + + const { permission, membership } = await getUserOrgPermissions(req.user._id, orgId); res.status(200).json({ data: { - permissions: packRules(permission.rules) + permissions: packRules(permission.rules), + membership } }); }; @@ -226,11 +227,12 @@ export const getUserWorkspacePermissions = async (req: Request, res: Response) = const { params: { workspaceId } } = await validateRequest(GetUserProjectPermission, req); - const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + const { permission, membership } = await getUserProjectPermissions(req.user._id, workspaceId); res.status(200).json({ data: { - permissions: packRules(permission.rules) + permissions: packRules(permission.rules), + membership } }); }; diff --git a/frontend/src/components/permissions/OrgPermissionCan.tsx b/frontend/src/components/permissions/OrgPermissionCan.tsx index 4366110a9..006d43232 100644 --- a/frontend/src/components/permissions/OrgPermissionCan.tsx +++ b/frontend/src/components/permissions/OrgPermissionCan.tsx @@ -21,7 +21,7 @@ export const OrgPermissionCan: FunctionComponent = ({ allowedLabel, ...props }) => { - const permission = useOrgPermission(); + const { permission } = useOrgPermission(); return ( diff --git a/frontend/src/context/OrgPermissionContext/OrgPermissionContext.tsx b/frontend/src/context/OrgPermissionContext/OrgPermissionContext.tsx index 5320ece6d..312ae0437 100644 --- a/frontend/src/context/OrgPermissionContext/OrgPermissionContext.tsx +++ b/frontend/src/context/OrgPermissionContext/OrgPermissionContext.tsx @@ -1,6 +1,7 @@ import { createContext, ReactNode, useContext } from "react"; import { useGetUserOrgPermissions } from "@app/hooks/api"; +import { OrgUser } from "@app/hooks/api/types"; import { useOrganization } from "../OrganizationContext"; import { TOrgPermission } from "./types"; @@ -9,7 +10,10 @@ type Props = { children: ReactNode; }; -const OrgPermissionContext = createContext(null); +const OrgPermissionContext = createContext(null); export const OrgPermissionProvider = ({ children }: Props): JSX.Element => { const { currentOrg } = useOrganization(); diff --git a/frontend/src/hoc/withPermission/withPermission.tsx b/frontend/src/hoc/withPermission/withPermission.tsx index e0c402af3..389d8509c 100644 --- a/frontend/src/hoc/withPermission/withPermission.tsx +++ b/frontend/src/hoc/withPermission/withPermission.tsx @@ -21,7 +21,7 @@ export const withPermission = ( { action, subject, className, containerClassName }: Props["abilities"]> ) => { const HOC = (hocProps: T) => { - const permission = useOrgPermission(); + const { permission } = useOrgPermission(); // akhilmhdh: Set as any due to casl/react ts type bug // REASON: casl due to its type checking can't seem to union even if union intersection is applied diff --git a/frontend/src/hooks/api/roles/queries.tsx b/frontend/src/hooks/api/roles/queries.tsx index 3e68aa57d..5694ccfd6 100644 --- a/frontend/src/hooks/api/roles/queries.tsx +++ b/frontend/src/hooks/api/roles/queries.tsx @@ -8,6 +8,7 @@ import { apiRequest } from "@app/config/request"; import { OrgPermissionSet } from "@app/context/OrgPermissionContext/types"; import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext/types"; +import { OrgUser } from "../users/types"; import { TGetRolesDTO, TGetUserOrgPermissionsDTO, @@ -63,13 +64,16 @@ export const useGetRoles = ({ orgId, workspaceId }: TGetRolesDTO) => }); const getUserOrgPermissions = async ({ orgId }: TGetUserOrgPermissionsDTO) => { - if (orgId === "") return []; + if (orgId === "") return { permissions: [], membership: null }; const { data } = await apiRequest.get<{ - data: { permissions: PackRule>>[] }; - }>(`/api/v1/roles/organization/${orgId}/permissions`, {}); - - return data.data.permissions; + data: { + permissions: PackRule>>[]; + membership: OrgUser; + }; + }>(`/api/v1/roles/organization/${orgId}/permissions`); + + return data.data; }; export const useGetUserOrgPermissions = ({ orgId }: TGetUserOrgPermissionsDTO) => @@ -78,9 +82,9 @@ export const useGetUserOrgPermissions = ({ orgId }: TGetUserOrgPermissionsDTO) = queryFn: () => getUserOrgPermissions({ orgId }), // enabled: Boolean(orgId), select: (data) => { - const rule = unpackRules>>(data); + const rule = unpackRules>>(data.permissions); const ability = createMongoAbility(rule, { conditionsMatcher }); - return ability; + return { permission: ability, membership: data.membership }; } }); diff --git a/frontend/src/views/IntegrationsPage/components/IntegrationsSection/IntegrationsSection.tsx b/frontend/src/views/IntegrationsPage/components/IntegrationsSection/IntegrationsSection.tsx index 9e23e7909..7a15c0721 100644 --- a/frontend/src/views/IntegrationsPage/components/IntegrationsSection/IntegrationsSection.tsx +++ b/frontend/src/views/IntegrationsPage/components/IntegrationsSection/IntegrationsSection.tsx @@ -54,7 +54,7 @@ export const IntegrationsSection = ({
)} - {!isBotActive && ( + {!isBotActive && Boolean(integrations.length) && (
@@ -119,7 +119,7 @@ export const IntegrationsSection = ({ {integrationSlugNameMapping[integration.integration]}
- {(integration.integration === "qovery") && ( + {integration.integration === "qovery" && (
@@ -162,7 +162,8 @@ export const IntegrationsSection = ({
)} - {((integration.integration === "checkly") || (integration.integration === "github")) && ( + {(integration.integration === "checkly" || + integration.integration === "github") && (
diff --git a/frontend/src/views/Settings/OrgSettingsPage/components/OrgDeleteSection/OrgDeleteSection.tsx b/frontend/src/views/Settings/OrgSettingsPage/components/OrgDeleteSection/OrgDeleteSection.tsx index dd810f1ac..a336355db 100644 --- a/frontend/src/views/Settings/OrgSettingsPage/components/OrgDeleteSection/OrgDeleteSection.tsx +++ b/frontend/src/views/Settings/OrgSettingsPage/components/OrgDeleteSection/OrgDeleteSection.tsx @@ -1,81 +1,70 @@ import { useRouter } from "next/router"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; -import { - Button, - DeleteActionModal -} from "@app/components/v2"; -import { useOrganization, useUser } from "@app/context"; -import { - useDeleteOrgById, - useGetOrgUsers -} from "@app/hooks/api"; +import { Button, DeleteActionModal } from "@app/components/v2"; +import { useOrganization, useOrgPermission } from "@app/context"; +import { useDeleteOrgById } from "@app/hooks/api"; import { usePopUp } from "@app/hooks/usePopUp"; import { navigateUserToOrg } from "@app/views/Login/Login.utils"; export const OrgDeleteSection = () => { - const router = useRouter(); - const { currentOrg } = useOrganization(); - const { user } = useUser(); - const { createNotification } = useNotificationContext(); - const { data: members } = useGetOrgUsers(currentOrg?._id ?? ""); - - const membershipOrg = members?.find((member) => member.user._id === user._id); + const router = useRouter(); + const { currentOrg } = useOrganization(); + const { createNotification } = useNotificationContext(); + const { membership } = useOrgPermission(); - const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ - "deleteOrg" - ] as const); + const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ + "deleteOrg" + ] as const); - const { mutateAsync, isLoading } = useDeleteOrgById(); - - const handleDeleteOrgSubmit = async () => { - try { - if (!currentOrg?._id) return; - - await mutateAsync({ - organizationId: currentOrg?._id - }); - - createNotification({ - text: "Successfully deleted organization", - type: "success" - }); + const { mutateAsync, isLoading } = useDeleteOrgById(); - await navigateUserToOrg(router); - - handlePopUpClose("deleteOrg"); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to delete organization", - type: "error" - }); - } + const handleDeleteOrgSubmit = async () => { + try { + if (!currentOrg?._id) return; + + await mutateAsync({ + organizationId: currentOrg?._id + }); + + createNotification({ + text: "Successfully deleted organization", + type: "success" + }); + + await navigateUserToOrg(router); + + handlePopUpClose("deleteOrg"); + } catch (err) { + console.error(err); + createNotification({ + text: "Failed to delete organization", + type: "error" + }); } + }; - return ( -
-

- Danger Zone -

- - handlePopUpToggle("deleteOrg", isOpen)} - deleteKey="confirm" - onDeleteApproved={handleDeleteOrgSubmit} - /> -
- ); -} \ No newline at end of file + return ( +
+

Danger Zone

+ + handlePopUpToggle("deleteOrg", isOpen)} + deleteKey="confirm" + onDeleteApproved={handleDeleteOrgSubmit} + /> +
+ ); +}; diff --git a/frontend/src/views/Settings/OrgSettingsPage/components/OrgGeneralTab/OrgGeneralTab.tsx b/frontend/src/views/Settings/OrgSettingsPage/components/OrgGeneralTab/OrgGeneralTab.tsx index d8e48966d..9c8279caf 100644 --- a/frontend/src/views/Settings/OrgSettingsPage/components/OrgGeneralTab/OrgGeneralTab.tsx +++ b/frontend/src/views/Settings/OrgSettingsPage/components/OrgGeneralTab/OrgGeneralTab.tsx @@ -1,24 +1,17 @@ -import { useOrganization, useUser } from "@app/context"; -import { useGetOrgUsers } from "@app/hooks/api"; +import { useOrgPermission } from "@app/context"; import { OrgDeleteSection } from "../OrgDeleteSection"; import { OrgIncidentContactsSection } from "../OrgIncidentContactsSection"; import { OrgNameChangeSection } from "../OrgNameChangeSection"; export const OrgGeneralTab = () => { - const { currentOrg } = useOrganization(); - const { user } = useUser(); - const { data: members } = useGetOrgUsers(currentOrg?._id ?? ""); - - const membershipOrg = members?.find((member) => member.user._id === user?._id); + const { membership } = useOrgPermission(); return (
- {(membershipOrg && membershipOrg.role === "admin") && ( - - )} + {membership && membership.role === "admin" && }
); }; diff --git a/frontend/src/views/Settings/OrgSettingsPage/components/OrgIncidentContactsSection/OrgIncidentContactsSection.tsx b/frontend/src/views/Settings/OrgSettingsPage/components/OrgIncidentContactsSection/OrgIncidentContactsSection.tsx index bc2547528..fbb4987a7 100644 --- a/frontend/src/views/Settings/OrgSettingsPage/components/OrgIncidentContactsSection/OrgIncidentContactsSection.tsx +++ b/frontend/src/views/Settings/OrgSettingsPage/components/OrgIncidentContactsSection/OrgIncidentContactsSection.tsx @@ -15,7 +15,7 @@ export const OrgIncidentContactsSection = () => { const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([ "addContact" ] as const); - const permission = useOrgPermission(); + const { permission } = useOrgPermission(); return (
From d4a5eb12e862f0c4c0f860ea7a7311dc5c48d09b Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Thu, 2 Nov 2023 11:54:01 +0200 Subject: [PATCH 29/33] Patch checkly integration --- frontend/src/pages/integrations/checkly/create.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/pages/integrations/checkly/create.tsx b/frontend/src/pages/integrations/checkly/create.tsx index 4db24b846..cd5ae71b7 100644 --- a/frontend/src/pages/integrations/checkly/create.tsx +++ b/frontend/src/pages/integrations/checkly/create.tsx @@ -100,7 +100,7 @@ export default function ChecklyCreateIntegrationPage() { appId: targetApp?.appId, sourceEnvironment: selectedSourceEnvironment, targetService: targetGroup?.name, - targetServiceId: String(targetGroup?.groupId), + targetServiceId: targetGroup?.groupId ? String(targetGroup?.groupId) : undefined, secretPath, metadata: { secretSuffix From 73c7b917ab16c44a1ef1527449c953621e948fc0 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Thu, 2 Nov 2023 17:17:20 -0400 Subject: [PATCH 30/33] update secret rotation intro --- .../platform/secret-rotation/overview.mdx | 51 +++++++++++-------- 1 file changed, 29 insertions(+), 22 deletions(-) diff --git a/docs/documentation/platform/secret-rotation/overview.mdx b/docs/documentation/platform/secret-rotation/overview.mdx index a11142106..284375401 100644 --- a/docs/documentation/platform/secret-rotation/overview.mdx +++ b/docs/documentation/platform/secret-rotation/overview.mdx @@ -1,37 +1,44 @@ ---- -title: "Secret Rotation Overview" -description: "Keep your credentials safe by rotation" ---- +# Secret Rotation Overview -Secret rotation is the process of periodically changing the values of secrets. This is done to reduce the risk of secrets being compromised and used to gain unauthorized access to systems or data. +## Introduction -Rotated secrets can be -1. API key for an external service -2. Database credentials +Secret rotation is a process that involves updating secret credentials periodically to minimize the risk of their compromise. +Rotating secrets helps prevent unauthorized access to systems and sensitive data by ensuring that old credentials are replaced with new ones regularly. -## How does the rotation happen? +Rotated secrets may include, but are not limited to: -There are four phases in secret rotation and its triggered periodically in an internval. +1. API keys for external services +2. Database credentials for various platforms -1. Creation +## Rotation Process -System will create secret by calling an external service like an API call, or randomly generate a value. -Now there exist three valid secrets. +The practice of rotating secrets is a systematic and interval-based operation, carried out in four fundamental phases. -2. Test +### 1. Creation -Test the new secret key by some check to ensure its working one. Thus only two will be considered active and the other is considered inactive. +The system initiates the rotation process by either making an API call to an external service or generating a new secret value internally. +Upon successful creation, the system will temporarily have three versions of the secret: -3. Deletion +- **Current active secret**: The one currently in use. +- **Future active secret (pending)**: The newly created secret, awaiting validation. +- **Previous active secret**: The old secret, soon to be retired. -System will remove the inactive secret and now there exist two valid secrets +### 2. Testing -4. Finish +The newly generated secret is subjected to a verification process to ensure its validity and functionality. +This involves conducting checks or tests that simulate actual operations the secret would perform. +Only the current active and the future active (pending) secrets are considered operational at this stage, while the previous active secret remains in standby mode. -System will switch the secret value from the rotated ones and trigger side effects like webhooks and events. +### 3. Deletion + +Post-verification, the system deactivates and deletes the previous active secret, leaving only the current and future active (pending) secrets in the system. + +### 4. Activation + +Finally, the system promotes the future active (pending) secret to be the new current active secret. It then triggers necessary side effects, such as invoking webhooks and generating events, to notify other services of the change. ## Infisical Secret Rotation Strategies -1. [SendGrid](./sendgrid) -2. [PostgreSQL/CockroachDB](./postgres) -3. [MySQL/MariaDB](./mysql) +1. [SendGrid Integration](./sendgrid) +2. [PostgreSQL/CockroachDB Implementation](./postgres) +3. [MySQL/MariaDB Configuration](./mysql) From db7a0649612d799266770f36afae1c98c65ee91b Mon Sep 17 00:00:00 2001 From: Akhil Mohan Date: Fri, 3 Nov 2023 20:01:51 +0530 Subject: [PATCH 31/33] feat: changed enable blindIndex from settings to overview page for attention --- frontend/src/hooks/api/workspace/queries.tsx | 20 +++- .../SecretOverviewPage/SecretOverviewPage.tsx | 2 + .../ProjectIndexSecretsSection.tsx | 98 +++++++++++++++++++ .../ProjectIndexSecretsSection/index.tsx | 0 .../CreateRotationForm/CreateRotationForm.tsx | 3 +- .../ProjectGeneralTab/ProjectGeneralTab.tsx | 24 +++-- .../ProjectIndexSecretsSection.tsx | 84 ---------------- .../ProjectSettingsPage/components/index.tsx | 1 - frontend/tsconfig.json | 27 +++-- 9 files changed, 145 insertions(+), 114 deletions(-) create mode 100644 frontend/src/views/SecretOverviewPage/components/ProjectIndexSecretsSection/ProjectIndexSecretsSection.tsx rename frontend/src/views/{Settings/ProjectSettingsPage => SecretOverviewPage}/components/ProjectIndexSecretsSection/index.tsx (100%) delete mode 100644 frontend/src/views/Settings/ProjectSettingsPage/components/ProjectIndexSecretsSection/ProjectIndexSecretsSection.tsx diff --git a/frontend/src/hooks/api/workspace/queries.tsx b/frontend/src/hooks/api/workspace/queries.tsx index e3136fae3..5b185b97a 100644 --- a/frontend/src/hooks/api/workspace/queries.tsx +++ b/frontend/src/hooks/api/workspace/queries.tsx @@ -34,7 +34,8 @@ export const workspaceKeys = { getUserWsEnvironments: (workspaceId: string) => ["workspace-env", { workspaceId }] as const, getWorkspaceAuditLogs: (workspaceId: string) => [{ workspaceId }] as const, getWorkspaceUsers: (workspaceId: string) => [{ workspaceId }] as const, - getWorkspaceServiceTokenDataV3: (workspaceId: string) => [{ workspaceId }, "workspace-service-token-data-v3"] as const + getWorkspaceServiceTokenDataV3: (workspaceId: string) => + [{ workspaceId }, "workspace-service-token-data-v3"] as const }; const fetchWorkspaceById = async (workspaceId: string) => { @@ -53,7 +54,7 @@ const fetchWorkspaceIndexStatus = async (workspaceId: string) => { return data; }; -const fetchWorkspaceSecrets = async (workspaceId: string) => { +export const fetchWorkspaceSecrets = async (workspaceId: string) => { const { data: { secrets } } = await apiRequest.get<{ secrets: EncryptedSecret[] }>( @@ -253,9 +254,18 @@ export const useReorderWsEnvironment = () => { const queryClient = useQueryClient(); return useMutation<{}, {}, ReorderEnvironmentsDTO>({ - mutationFn: ({ workspaceID, environmentSlug, environmentName, otherEnvironmentSlug, otherEnvironmentName}) => { + mutationFn: ({ + workspaceID, + environmentSlug, + environmentName, + otherEnvironmentSlug, + otherEnvironmentName + }) => { return apiRequest.patch(`/api/v2/workspace/${workspaceID}/environments`, { - environmentSlug, environmentName, otherEnvironmentSlug, otherEnvironmentName + environmentSlug, + environmentName, + otherEnvironmentSlug, + otherEnvironmentName }); }, onSuccess: () => { @@ -378,4 +388,4 @@ export const useGetWorkspaceServiceTokenDataV3 = (workspaceId: string) => { }, enabled: true }); -}; \ No newline at end of file +}; diff --git a/frontend/src/views/SecretOverviewPage/SecretOverviewPage.tsx b/frontend/src/views/SecretOverviewPage/SecretOverviewPage.tsx index 7667eaeb6..0cb590631 100644 --- a/frontend/src/views/SecretOverviewPage/SecretOverviewPage.tsx +++ b/frontend/src/views/SecretOverviewPage/SecretOverviewPage.tsx @@ -41,6 +41,7 @@ import { } from "@app/hooks/api"; import { FolderBreadCrumbs } from "./components/FolderBreadCrumbs"; +import { ProjectIndexSecretsSection } from "./components/ProjectIndexSecretsSection"; import { SecretOverviewFolderRow } from "./components/SecretOverviewFolderRow"; import { SecretOverviewTableRow } from "./components/SecretOverviewTableRow"; @@ -259,6 +260,7 @@ export const SecretOverviewPage = () => { return (
+
diff --git a/frontend/src/views/SecretOverviewPage/components/ProjectIndexSecretsSection/ProjectIndexSecretsSection.tsx b/frontend/src/views/SecretOverviewPage/components/ProjectIndexSecretsSection/ProjectIndexSecretsSection.tsx new file mode 100644 index 000000000..92f1bee37 --- /dev/null +++ b/frontend/src/views/SecretOverviewPage/components/ProjectIndexSecretsSection/ProjectIndexSecretsSection.tsx @@ -0,0 +1,98 @@ +import { ProjectPermissionCan } from "@app/components/permissions"; +import { + decryptAssymmetric, + decryptSymmetric +} from "@app/components/utilities/cryptography/crypto"; +import { Button, Spinner } from "@app/components/v2"; +import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; +import { useToggle } from "@app/hooks"; +import { useGetWorkspaceIndexStatus, useNameWorkspaceSecrets } from "@app/hooks/api"; +import { UserWsKeyPair } from "@app/hooks/api/types"; +import { fetchWorkspaceSecrets } from "@app/hooks/api/workspace/queries"; + +// TODO: add check so that this only shows up if user is +// an admin in the workspace +type Props = { + decryptFileKey: UserWsKeyPair; +}; + +export const ProjectIndexSecretsSection = ({ decryptFileKey }: Props) => { + const { currentWorkspace } = useWorkspace(); + const { data: isBlindIndexed, isLoading: isBlindIndexedLoading } = useGetWorkspaceIndexStatus( + currentWorkspace?._id ?? "" + ); + const [isIndexing, setIsIndexing] = useToggle(); + const nameWorkspaceSecrets = useNameWorkspaceSecrets(); + + const onEnableBlindIndices = async () => { + if (!currentWorkspace?._id) return; + setIsIndexing.on(); + try { + const encryptedSecrets = await fetchWorkspaceSecrets(currentWorkspace._id); + + const key = decryptAssymmetric({ + ciphertext: decryptFileKey.encryptedKey, + nonce: decryptFileKey.nonce, + publicKey: decryptFileKey.sender.publicKey, + privateKey: localStorage.getItem("PRIVATE_KEY") as string + }); + + const secretsToUpdate = encryptedSecrets.map((encryptedSecret) => { + const secretName = decryptSymmetric({ + ciphertext: encryptedSecret.secretKeyCiphertext, + iv: encryptedSecret.secretKeyIV, + tag: encryptedSecret.secretKeyTag, + key + }); + + return { + secretName, + _id: encryptedSecret._id + }; + }); + await nameWorkspaceSecrets.mutateAsync({ + workspaceId: currentWorkspace._id, + secretsToUpdate + }); + } catch (err) { + console.log(err); + } finally { + setIsIndexing.off(); + } + }; + + return !isBlindIndexedLoading && !isBlindIndexed ? ( +
+ {isIndexing && ( +
+ +
+
Please wait
+ Re-indexing your secrets... +
+
+ )} +

Enable Blind Indices

+

+ Your project, created before the introduction of blind indexing, contains unindexed secrets. + To access individual secrets by name through the SDK and public API, please enable blind + indexing. This is a one time process. +

+ + {(isAllowed) => ( + + )} + +
+ ) : ( +
+ ); +}; diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/ProjectIndexSecretsSection/index.tsx b/frontend/src/views/SecretOverviewPage/components/ProjectIndexSecretsSection/index.tsx similarity index 100% rename from frontend/src/views/Settings/ProjectSettingsPage/components/ProjectIndexSecretsSection/index.tsx rename to frontend/src/views/SecretOverviewPage/components/ProjectIndexSecretsSection/index.tsx diff --git a/frontend/src/views/SecretRotationPage/components/CreateRotationForm/CreateRotationForm.tsx b/frontend/src/views/SecretRotationPage/components/CreateRotationForm/CreateRotationForm.tsx index 3576cc0f9..5d4ed8073 100644 --- a/frontend/src/views/SecretRotationPage/components/CreateRotationForm/CreateRotationForm.tsx +++ b/frontend/src/views/SecretRotationPage/components/CreateRotationForm/CreateRotationForm.tsx @@ -1,12 +1,11 @@ import { useRef, useState } from "react"; import { AnimatePresence, motion } from "framer-motion"; +import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import { Modal, ModalContent, Step, Stepper } from "@app/components/v2"; import { useCreateSecretRotation } from "@app/hooks/api"; import { TSecretRotationProvider } from "@app/hooks/api/types"; -import { useNotificationContext } from "~/components/context/Notifications/NotificationProvider"; - import { RotationInputForm } from "./steps/RotationInputForm"; import { RotationOutputForm, diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/ProjectGeneralTab/ProjectGeneralTab.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/ProjectGeneralTab/ProjectGeneralTab.tsx index cacfda8bb..bbd8e78d4 100644 --- a/frontend/src/views/Settings/ProjectSettingsPage/components/ProjectGeneralTab/ProjectGeneralTab.tsx +++ b/frontend/src/views/Settings/ProjectSettingsPage/components/ProjectGeneralTab/ProjectGeneralTab.tsx @@ -2,20 +2,18 @@ import { AutoCapitalizationSection } from "../AutoCapitalizationSection"; import { DeleteProjectSection } from "../DeleteProjectSection"; import { E2EESection } from "../E2EESection"; import { EnvironmentSection } from "../EnvironmentSection"; -import { ProjectIndexSecretsSection } from "../ProjectIndexSecretsSection"; import { ProjectNameChangeSection } from "../ProjectNameChangeSection"; import { SecretTagsSection } from "../SecretTagsSection"; export const ProjectGeneralTab = () => { - return ( -
- - - - - - - -
- ); -} \ No newline at end of file + return ( +
+ + + + + + +
+ ); +}; diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/ProjectIndexSecretsSection/ProjectIndexSecretsSection.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/ProjectIndexSecretsSection/ProjectIndexSecretsSection.tsx deleted file mode 100644 index ca2e14eb9..000000000 --- a/frontend/src/views/Settings/ProjectSettingsPage/components/ProjectIndexSecretsSection/ProjectIndexSecretsSection.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import { ProjectPermissionCan } from "@app/components/permissions"; -import { - decryptAssymmetric, - decryptSymmetric -} from "@app/components/utilities/cryptography/crypto"; -import { Button } from "@app/components/v2"; -import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; -import { - useGetUserWsKey, - useGetWorkspaceIndexStatus, - useGetWorkspaceSecrets, - useNameWorkspaceSecrets -} from "@app/hooks/api"; - -// TODO: add check so that this only shows up if user is -// an admin in the workspace - -export const ProjectIndexSecretsSection = () => { - const { currentWorkspace } = useWorkspace(); - const { data: isBlindIndexed, isLoading: isBlindIndexedLoading } = useGetWorkspaceIndexStatus( - currentWorkspace?._id ?? "" - ); - const { data: latestFileKey } = useGetUserWsKey(currentWorkspace?._id ?? ""); - const { data: encryptedSecrets } = useGetWorkspaceSecrets(currentWorkspace?._id ?? ""); - const nameWorkspaceSecrets = useNameWorkspaceSecrets(); - - const onEnableBlindIndices = async () => { - if (!currentWorkspace?._id) return; - if (!encryptedSecrets) return; - if (!latestFileKey) return; - - const key = decryptAssymmetric({ - ciphertext: latestFileKey.encryptedKey, - nonce: latestFileKey.nonce, - publicKey: latestFileKey.sender.publicKey, - privateKey: localStorage.getItem("PRIVATE_KEY") as string - }); - - const secretsToUpdate = encryptedSecrets.map((encryptedSecret) => { - const secretName = decryptSymmetric({ - ciphertext: encryptedSecret.secretKeyCiphertext, - iv: encryptedSecret.secretKeyIV, - tag: encryptedSecret.secretKeyTag, - key - }); - - return { - secretName, - _id: encryptedSecret._id - }; - }); - - await nameWorkspaceSecrets.mutateAsync({ - workspaceId: currentWorkspace._id, - secretsToUpdate - }); - }; - - return !isBlindIndexedLoading && !isBlindIndexed ? ( -
-

Blind Indices

-

- Your project, created before the introduction of blind indexing, contains unindexed secrets. - To access individual secrets by name through the SDK and public API, please enable blind - indexing. -

- - {(isAllowed) => ( - - )} - -
- ) : ( -
- ); -}; diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/index.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/index.tsx index 5f9e1013b..757dfc6b2 100644 --- a/frontend/src/views/Settings/ProjectSettingsPage/components/index.tsx +++ b/frontend/src/views/Settings/ProjectSettingsPage/components/index.tsx @@ -2,7 +2,6 @@ export { AutoCapitalizationSection } from "./AutoCapitalizationSection"; export { DeleteProjectSection } from "./DeleteProjectSection"; export { E2EESection } from "./E2EESection"; export { EnvironmentSection } from "./EnvironmentSection"; -export { ProjectIndexSecretsSection } from "./ProjectIndexSecretsSection"; export { ProjectNameChangeSection } from "./ProjectNameChangeSection"; export { SecretTagsSection } from "./SecretTagsSection"; export { ServiceTokenSection } from "./ServiceTokenSection"; diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index d5a16121a..42ea0e773 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -2,15 +2,16 @@ "compilerOptions": { "baseUrl": ".", "paths": { - "~/components/*": ["./src/components/*"], - "~/hooks/*": ["./src/hooks/*"], - "~/utilities/*": ["./src/components/utilities/*"], - "~/*": ["./src/const"], - "~/pages/*": ["./src/pages/*"], - "@app/*": ["./src/*"] + "@app/*": [ + "./src/*" + ] }, "target": "ESNext", - "lib": ["dom", "dom.iterable", "esnext"], + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], "allowJs": true, "skipLibCheck": true, "strict": true, @@ -25,6 +26,14 @@ "jsx": "preserve", "incremental": true }, - "include": ["next-i18next.config.js", "next-env.d.ts", "./src/**/*.ts", "./src/**/*.tsx", "./.eslintrc.js"], - "exclude": ["node_modules"] + "include": [ + "next-i18next.config.js", + "next-env.d.ts", + "./src/**/*.ts", + "./src/**/*.tsx", + "./.eslintrc.js" + ], + "exclude": [ + "node_modules" + ] } From 3402acb05cdeb5f7df491492ce2730fa728d9c92 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Fri, 3 Nov 2023 16:00:00 -0400 Subject: [PATCH 32/33] update blind indexing message --- .../ProjectIndexSecretsSection/ProjectIndexSecretsSection.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/views/SecretOverviewPage/components/ProjectIndexSecretsSection/ProjectIndexSecretsSection.tsx b/frontend/src/views/SecretOverviewPage/components/ProjectIndexSecretsSection/ProjectIndexSecretsSection.tsx index 92f1bee37..23cfa750d 100644 --- a/frontend/src/views/SecretOverviewPage/components/ProjectIndexSecretsSection/ProjectIndexSecretsSection.tsx +++ b/frontend/src/views/SecretOverviewPage/components/ProjectIndexSecretsSection/ProjectIndexSecretsSection.tsx @@ -74,8 +74,8 @@ export const ProjectIndexSecretsSection = ({ decryptFileKey }: Props) => { )}

Enable Blind Indices

- Your project, created before the introduction of blind indexing, contains unindexed secrets. - To access individual secrets by name through the SDK and public API, please enable blind + Your project was created before the introduction of blind indexing. + To continue accessing secrets by name through the SDK, public API and web dashboard, please enable blind indexing. This is a one time process.

From 176d92546c40ad23e86c1a176bbe41519e76c4ac Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Fri, 3 Nov 2023 22:37:50 +0200 Subject: [PATCH 33/33] Split ST V3 modal into option tabs, re-modularized authn methods --- backend/src/helpers/secrets.ts | 2 +- backend/src/middleware/requireAuth.ts | 2 +- .../utils/authn/authDataExtractors/index.ts | 53 -- backend/src/utils/authn/authMode/index.ts | 4 - .../apiKey.ts | 0 .../apiKeyV2.ts | 0 .../utils/authn/authModeValidators/index.ts | 5 + .../{authMode => authModeValidators}/jwt.ts | 0 .../serviceTokenV2.ts | 0 .../serviceTokenV3.ts | 0 .../utils/authn/helpers/authDataExtractors.ts | 53 ++ .../{authMode/helpers.ts => helpers/index.ts} | 16 +- .../PersonalAPIKeyTab/PersonalAPIKeyTab.tsx | 4 +- .../ProjectServiceTokensTab.tsx | 4 +- .../AddServiceTokenV3Modal.tsx | 491 ++++++++++-------- 15 files changed, 336 insertions(+), 298 deletions(-) delete mode 100644 backend/src/utils/authn/authDataExtractors/index.ts delete mode 100644 backend/src/utils/authn/authMode/index.ts rename backend/src/utils/authn/{authMode => authModeValidators}/apiKey.ts (100%) rename backend/src/utils/authn/{authMode => authModeValidators}/apiKeyV2.ts (100%) create mode 100644 backend/src/utils/authn/authModeValidators/index.ts rename backend/src/utils/authn/{authMode => authModeValidators}/jwt.ts (100%) rename backend/src/utils/authn/{authMode => authModeValidators}/serviceTokenV2.ts (100%) rename backend/src/utils/authn/{authMode => authModeValidators}/serviceTokenV3.ts (100%) create mode 100644 backend/src/utils/authn/helpers/authDataExtractors.ts rename backend/src/utils/authn/{authMode/helpers.ts => helpers/index.ts} (95%) diff --git a/backend/src/helpers/secrets.ts b/backend/src/helpers/secrets.ts index beacf709d..f6252d5c6 100644 --- a/backend/src/helpers/secrets.ts +++ b/backend/src/helpers/secrets.ts @@ -49,7 +49,7 @@ import { import { TelemetryService } from "../services"; import { client, getEncryptionKey, getRootEncryptionKey } from "../config"; import { EEAuditLogService, EELogService, EESecretService } from "../ee/services"; -import { getAuthDataPayloadIdObj, getAuthDataPayloadUserObj } from "../utils/authn/authDataExtractors"; +import { getAuthDataPayloadIdObj, getAuthDataPayloadUserObj } from "../utils/authn/helpers"; import { getFolderByPath, getFolderIdFromServiceToken } from "../services/FolderService"; import picomatch from "picomatch"; import path from "path"; diff --git a/backend/src/middleware/requireAuth.ts b/backend/src/middleware/requireAuth.ts index 87672feb9..929d22fea 100644 --- a/backend/src/middleware/requireAuth.ts +++ b/backend/src/middleware/requireAuth.ts @@ -2,7 +2,7 @@ import jwt from "jsonwebtoken"; import { NextFunction, Request, Response } from "express"; import { AuthMode } from "../variables"; import { AuthData } from "../interfaces/middleware"; -import { extractAuthMode, getAuthData } from "../utils/authn/authMode"; +import { extractAuthMode, getAuthData } from "../utils/authn/helpers"; import { UnauthorizedRequestError } from "../utils/errors"; declare module "jsonwebtoken" { diff --git a/backend/src/utils/authn/authDataExtractors/index.ts b/backend/src/utils/authn/authDataExtractors/index.ts deleted file mode 100644 index 89c1f419e..000000000 --- a/backend/src/utils/authn/authDataExtractors/index.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { AuthData } from "../../../interfaces/middleware"; -import { - ServiceAccount, - ServiceTokenData, - ServiceTokenDataV3, - User -} from "../../../models"; - -/** - * Returns an object containing the id of the authentication data payload - * @param {AuthData} authData - authentication data object - * @returns - */ -export const getAuthDataPayloadIdObj = (authData: AuthData) => { - if (authData.authPayload instanceof User) { - return { userId: authData.authPayload._id }; - } - - if (authData.authPayload instanceof ServiceAccount) { - return { serviceAccountId: authData.authPayload._id }; - } - - if (authData.authPayload instanceof ServiceTokenData) { - return { serviceTokenDataId: authData.authPayload._id }; - } - - if (authData.authPayload instanceof ServiceTokenDataV3) { - return { serviceTokenDataId: authData.authPayload._id }; - } -}; - -/** - * Returns an object containing the user associated with the authentication data payload - * @param {AuthData} authData - authentication data object - * @returns - */ -export const getAuthDataPayloadUserObj = (authData: AuthData) => { - if (authData.authPayload instanceof User) { - return { user: authData.authPayload._id }; - } - - if (authData.authPayload instanceof ServiceAccount) { - return { user: authData.authPayload.user }; - } - - if (authData.authPayload instanceof ServiceTokenData) { - return { user: authData.authPayload.user }; - } - - if (authData.authPayload instanceof ServiceTokenDataV3) { - return { user: authData.authPayload.user }; - } -} \ No newline at end of file diff --git a/backend/src/utils/authn/authMode/index.ts b/backend/src/utils/authn/authMode/index.ts deleted file mode 100644 index 7d7262551..000000000 --- a/backend/src/utils/authn/authMode/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { - extractAuthMode, - getAuthData -} from "./helpers"; \ No newline at end of file diff --git a/backend/src/utils/authn/authMode/apiKey.ts b/backend/src/utils/authn/authModeValidators/apiKey.ts similarity index 100% rename from backend/src/utils/authn/authMode/apiKey.ts rename to backend/src/utils/authn/authModeValidators/apiKey.ts diff --git a/backend/src/utils/authn/authMode/apiKeyV2.ts b/backend/src/utils/authn/authModeValidators/apiKeyV2.ts similarity index 100% rename from backend/src/utils/authn/authMode/apiKeyV2.ts rename to backend/src/utils/authn/authModeValidators/apiKeyV2.ts diff --git a/backend/src/utils/authn/authModeValidators/index.ts b/backend/src/utils/authn/authModeValidators/index.ts new file mode 100644 index 000000000..0ac4c3c1c --- /dev/null +++ b/backend/src/utils/authn/authModeValidators/index.ts @@ -0,0 +1,5 @@ +export * from "./apiKey"; +export * from "./apiKeyV2"; +export * from "./jwt"; +export * from "./serviceTokenV2"; +export * from "./serviceTokenV3"; \ No newline at end of file diff --git a/backend/src/utils/authn/authMode/jwt.ts b/backend/src/utils/authn/authModeValidators/jwt.ts similarity index 100% rename from backend/src/utils/authn/authMode/jwt.ts rename to backend/src/utils/authn/authModeValidators/jwt.ts diff --git a/backend/src/utils/authn/authMode/serviceTokenV2.ts b/backend/src/utils/authn/authModeValidators/serviceTokenV2.ts similarity index 100% rename from backend/src/utils/authn/authMode/serviceTokenV2.ts rename to backend/src/utils/authn/authModeValidators/serviceTokenV2.ts diff --git a/backend/src/utils/authn/authMode/serviceTokenV3.ts b/backend/src/utils/authn/authModeValidators/serviceTokenV3.ts similarity index 100% rename from backend/src/utils/authn/authMode/serviceTokenV3.ts rename to backend/src/utils/authn/authModeValidators/serviceTokenV3.ts diff --git a/backend/src/utils/authn/helpers/authDataExtractors.ts b/backend/src/utils/authn/helpers/authDataExtractors.ts new file mode 100644 index 000000000..60639739e --- /dev/null +++ b/backend/src/utils/authn/helpers/authDataExtractors.ts @@ -0,0 +1,53 @@ +import { AuthData } from "../../../interfaces/middleware"; +import { + ServiceAccount, + ServiceTokenData, + ServiceTokenDataV3, + User +} from "../../../models"; + +/** + * Returns an object containing the id of the authentication data payload + * @param {AuthData} authData - authentication data object + * @returns + */ + export const getAuthDataPayloadIdObj = (authData: AuthData) => { + if (authData.authPayload instanceof User) { + return { userId: authData.authPayload._id }; + } + + if (authData.authPayload instanceof ServiceAccount) { + return { serviceAccountId: authData.authPayload._id }; + } + + if (authData.authPayload instanceof ServiceTokenData) { + return { serviceTokenDataId: authData.authPayload._id }; + } + + if (authData.authPayload instanceof ServiceTokenDataV3) { + return { serviceTokenDataId: authData.authPayload._id }; + } +}; + +/** + * Returns an object containing the user associated with the authentication data payload + * @param {AuthData} authData - authentication data object + * @returns + */ +export const getAuthDataPayloadUserObj = (authData: AuthData) => { + if (authData.authPayload instanceof User) { + return { user: authData.authPayload._id }; + } + + if (authData.authPayload instanceof ServiceAccount) { + return { user: authData.authPayload.user }; + } + + if (authData.authPayload instanceof ServiceTokenData) { + return { user: authData.authPayload.user }; + } + + if (authData.authPayload instanceof ServiceTokenDataV3) { + return { user: authData.authPayload.user }; + } +} \ No newline at end of file diff --git a/backend/src/utils/authn/authMode/helpers.ts b/backend/src/utils/authn/helpers/index.ts similarity index 95% rename from backend/src/utils/authn/authMode/helpers.ts rename to backend/src/utils/authn/helpers/index.ts index ecb852f61..064efe133 100644 --- a/backend/src/utils/authn/authMode/helpers.ts +++ b/backend/src/utils/authn/helpers/index.ts @@ -1,15 +1,19 @@ +import { AuthData } from "../../../interfaces/middleware"; import jwt from "jsonwebtoken"; import { getAuthSecret } from "../../../config"; import { ActorType } from "../../../ee/models"; import { AuthMode, AuthTokenType } from "../../../variables"; import { UnauthorizedRequestError } from "../../errors"; -import { validateAPIKey } from "./apiKey"; -import { validateAPIKeyV2 } from "./apiKeyV2"; -import { validateServiceTokenV2 } from "./serviceTokenV2"; -import { validateServiceTokenV3 } from "./serviceTokenV3"; -import { validateJWT } from "./jwt"; +import { + validateAPIKey, + validateAPIKeyV2, + validateJWT, + validateServiceTokenV2, + validateServiceTokenV3 +} from "../authModeValidators"; import { getUserAgentType } from "../../posthog"; -import { AuthData } from "../../../interfaces/middleware"; + +export * from "./authDataExtractors"; interface ExtractAuthModeParams { headers: { [key: string]: string | string[] | undefined } diff --git a/frontend/src/views/Settings/PersonalSettingsPage/PersonalAPIKeyTab/PersonalAPIKeyTab.tsx b/frontend/src/views/Settings/PersonalSettingsPage/PersonalAPIKeyTab/PersonalAPIKeyTab.tsx index 035f0b322..aba48b23e 100644 --- a/frontend/src/views/Settings/PersonalSettingsPage/PersonalAPIKeyTab/PersonalAPIKeyTab.tsx +++ b/frontend/src/views/Settings/PersonalSettingsPage/PersonalAPIKeyTab/PersonalAPIKeyTab.tsx @@ -1,10 +1,10 @@ import { APIKeySection } from "../APIKeySection"; -import { APIKeyV2Section } from "../APIKeyV2Section"; +// import { APIKeyV2Section } from "../APIKeyV2Section"; export const PersonalAPIKeyTab = () => { return ( <> - + {/* */} ); diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/ProjectServiceTokensTab/ProjectServiceTokensTab.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/ProjectServiceTokensTab/ProjectServiceTokensTab.tsx index 12312dc78..d4f8e79e7 100644 --- a/frontend/src/views/Settings/ProjectSettingsPage/components/ProjectServiceTokensTab/ProjectServiceTokensTab.tsx +++ b/frontend/src/views/Settings/ProjectSettingsPage/components/ProjectServiceTokensTab/ProjectServiceTokensTab.tsx @@ -1,10 +1,10 @@ import { ServiceTokenSection } from "../ServiceTokenSection"; -import { ServiceTokenV3Section } from "../ServiceTokenV3Section"; +// import { ServiceTokenV3Section } from "../ServiceTokenV3Section"; export const ProjectServiceTokensTab = () => { return ( <> - + {/* */} ); diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenV3Section/AddServiceTokenV3Modal.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenV3Section/AddServiceTokenV3Modal.tsx index c1779b19e..3d76b2435 100644 --- a/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenV3Section/AddServiceTokenV3Modal.tsx +++ b/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenV3Section/AddServiceTokenV3Modal.tsx @@ -1,11 +1,13 @@ import { useEffect, useState } from "react"; import { Controller, useFieldArray, useForm } from "react-hook-form"; -import { faPlus, faXmark, faCheck, faCopy } from "@fortawesome/free-solid-svg-icons"; +import { faCheck, faCopy,faPlus, faXmark } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { yupResolver } from "@hookform/resolvers/yup"; +import { motion } from "framer-motion"; import nacl from "tweetnacl"; import { encodeBase64 } from "tweetnacl-util"; import * as yup from "yup"; + import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import { decryptAssymmetric, @@ -21,8 +23,11 @@ import { Select, SelectItem, Switch, - UpgradePlanModal -} from "@app/components/v2"; + Tab, + TabList, + TabPanel, + Tabs, + UpgradePlanModal} from "@app/components/v2"; import { useSubscription, useWorkspace @@ -42,6 +47,11 @@ import { } from "@app/hooks/api/serviceTokens/types"; import { UsePopUpState } from "@app/hooks/usePopUp"; +enum TabSections { + General = "general", + Advanced = "advanced" +} + const expirations = [ { label: "Never", value: "" }, { label: "1 day", value: "86400" }, @@ -343,246 +353,269 @@ export const AddServiceTokenV3Modal = ({ {!hasServiceTokenJSON ? ( - ( - + +
+ General + Advanced +
+
+ + - -
- )} - /> - {tokenScopes.map(({ id }, index) => ( -
- ( - - - - )} - /> - ( - - - - )} - /> - ( - - - - )} - /> - remove(index)} - size="lg" - colorSchema="danger" - variant="plain" - ariaLabel="update" - className="p-3" - > - - -
- ))} -
- -
- {tokenTrustedIps.map(({ id }, index) => ( -
- { - return ( + ( { + {...field} + placeholder="My ST V3" + /> + + )} + /> + {tokenScopes.map(({ id }, index) => ( +
+ ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> + remove(index)} + size="lg" + colorSchema="danger" + variant="plain" + ariaLabel="update" + className="p-3" + > + + +
+ ))} +
+ +
+ ( + + + + )} + /> + + + +
+ {tokenTrustedIps.map(({ id }, index) => ( +
+ { + return ( + + { + if (subscription?.ipAllowlisting) { + field.onChange(e); + return; + } + + handlePopUpOpen("upgradePlan"); + }} + placeholder="123.456.789.0" + /> + + ); + }} + /> + { if (subscription?.ipAllowlisting) { - field.onChange(e); + removeTrustedIp(index); return; } handlePopUpOpen("upgradePlan"); }} - placeholder="123.456.789.0" + size="lg" + colorSchema="danger" + variant="plain" + ariaLabel="update" + className="p-3" + > + + +
+ ))} +
+ +
+ ( + + - ); - }} - /> - { - if (subscription?.ipAllowlisting) { - removeTrustedIp(index); - return; - } - - handlePopUpOpen("upgradePlan"); - }} - size="lg" - colorSchema="danger" - variant="plain" - ariaLabel="update" - className="p-3" - > - - -
- ))} -
- -
- ( - - - - )} - /> - ( - - - - )} - /> -
- ( - onChange(isChecked)} - isChecked={value} - > - Refresh Token Rotation - - )} - /> -
-
+ )} + /> +
+ ( + onChange(isChecked)} + isChecked={value} + > + Refresh Token Rotation + + )} + /> +

When enabled, as a result of exchanging a refresh token, a new refresh token will be issued and the existing token will be invalidated.

+
+
+
+ +