From c9c40521b258c6f5c624aa23601e957b6b4b7f32 Mon Sep 17 00:00:00 2001 From: Akhil Mohan Date: Thu, 26 Oct 2023 14:12:39 +0530 Subject: [PATCH] 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) => ( ( )}