diff --git a/backend/src/controllers/v1/index.ts b/backend/src/controllers/v1/index.ts index c2bb1b94d..b32debeca 100644 --- a/backend/src/controllers/v1/index.ts +++ b/backend/src/controllers/v1/index.ts @@ -16,8 +16,6 @@ import * as workspaceController from "./workspaceController"; import * as secretScanningController from "./secretScanningController"; import * as webhookController from "./webhookController"; import * as secretImpsController from "./secretImpsController"; -import * as secretApprovalPolicyController from "./secretApprovalPolicyController"; - export { authController, botController, @@ -36,6 +34,5 @@ export { workspaceController, secretScanningController, webhookController, - secretImpsController, - secretApprovalPolicyController + secretImpsController }; diff --git a/backend/src/controllers/v3/secretsController.ts b/backend/src/controllers/v3/secretsController.ts index 482e5baa6..31a727e02 100644 --- a/backend/src/controllers/v3/secretsController.ts +++ b/backend/src/controllers/v3/secretsController.ts @@ -3,21 +3,11 @@ import { Types } from "mongoose"; import { EventService, SecretService } from "../../services"; import { eventPushSecrets } from "../../events"; import { BotService } from "../../services"; -import { - containsGlobPatterns, - isValidScopeV3, - repackageSecretToRaw -} from "../../helpers/secrets"; +import { containsGlobPatterns, isValidScopeV3, repackageSecretToRaw } from "../../helpers/secrets"; import { encryptSymmetric128BitHexKeyUTF8 } from "../../utils/crypto"; import { getAllImportedSecrets } from "../../services/SecretImportService"; -import { - Folder, - IServiceTokenData, - IServiceTokenDataV3 -} from "../../models"; -import { - Permission -} from "../../models/serviceTokenDataV3"; +import { Folder, IMembership, IServiceTokenData, IServiceTokenDataV3 } from "../../models"; +import { Permission } from "../../models/serviceTokenDataV3"; import { getFolderByPath, getFolderWithPathFromId } from "../../services/FolderService"; import { BadRequestError } from "../../utils/errors"; import { validateRequest } from "../../helpers/validation"; @@ -28,7 +18,7 @@ import { getUserProjectPermissions } from "../../ee/services/ProjectRoleService"; import { ForbiddenError, subject } from "@casl/ability"; -import { +import { validateServiceTokenDataClientForWorkspace, validateServiceTokenDataV3ClientForWorkspace } from "../../validation"; @@ -36,6 +26,12 @@ import { PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS } from "../../variabl import { ActorType } from "../../ee/models"; import { UnauthorizedRequestError } from "../../utils/errors"; import { AuthData } from "../../interfaces/middleware"; +import { + generateSecretApprovalRequest, + getSecretPolicyOfBoard +} from "../../ee/services/SecretApprovalService"; +import { CommitType } from "../../ee/models/secretApprovalRequest"; +import { IRole } from "../../ee/models/role"; const checkSecretsPermission = async ({ authData, @@ -49,8 +45,10 @@ const checkSecretsPermission = async ({ environment: string; secretPath: string; secretAction: ProjectPermissionActions; // CRUD -}): Promise<(env: string, secPath: string) => boolean> => { - +}): Promise<{ + authVerifier: (env: string, secPath: string) => boolean; + membership?: Omit & { customRole: IRole }; +}> => { let STV2RequiredPermissions = []; let STV3RequiredPermissions: Permission[] = []; @@ -72,22 +70,28 @@ const checkSecretsPermission = async ({ STV3RequiredPermissions = [Permission.WRITE]; break; } - + switch (authData.actor.type) { case ActorType.USER: { - const { permission } = await getUserProjectPermissions(authData.actor.metadata.userId, workspaceId); + const { permission, membership } = await getUserProjectPermissions( + authData.actor.metadata.userId, + workspaceId + ); ForbiddenError.from(permission).throwUnlessCan( secretAction, subject(ProjectPermissionSub.Secrets, { environment, secretPath }) ); - return (env: string, secPath: string) => - permission.can( - secretAction, - subject(ProjectPermissionSub.Secrets, { - environment: env, - secretPath: secPath - }) - ); + return { + authVerifier: (env: string, secPath: string) => + permission.can( + secretAction, + subject(ProjectPermissionSub.Secrets, { + environment: env, + secretPath: secPath + }) + ), + membership + }; } case ActorType.SERVICE: { await validateServiceTokenDataClientForWorkspace({ @@ -97,7 +101,7 @@ const checkSecretsPermission = async ({ secretPath, requiredPermissions: STV2RequiredPermissions }); - return () => true; + return { authVerifier: () => true }; } case ActorType.SERVICE_V3: { await validateServiceTokenDataV3ClientForWorkspace({ @@ -108,19 +112,21 @@ const checkSecretsPermission = async ({ secretPath, requiredPermissions: STV3RequiredPermissions }); - return (env: string, secPath: string) => - isValidScopeV3({ - authPayload: authData.authPayload as IServiceTokenDataV3, - environment: env, - secretPath: secPath, - requiredPermissions: STV3RequiredPermissions - }); + return { + authVerifier: (env: string, secPath: string) => + isValidScopeV3({ + authPayload: authData.authPayload as IServiceTokenDataV3, + environment: env, + secretPath: secPath, + requiredPermissions: STV3RequiredPermissions + }) + }; } default: { throw UnauthorizedRequestError(); } } -} +}; /** * Return secrets for workspace with id [workspaceId] and environment @@ -160,7 +166,7 @@ export const getSecretsRaw = async (req: Request, res: Response) => { if (!environment || !workspaceId) throw BadRequestError({ message: "Missing environment or workspace id" }); - const permissionCheckFn = await checkSecretsPermission({ + const { authVerifier: permissionCheckFn } = await checkSecretsPermission({ authData: req.authData, workspaceId, environment, @@ -232,7 +238,7 @@ export const getSecretByNameRaw = async (req: Request, res: Response) => { query: { secretPath, environment, workspaceId, type, include_imports }, params: { secretName } } = await validateRequest(reqValidator.GetSecretByNameRawV3, req); - + await checkSecretsPermission({ authData: req.authData, workspaceId, @@ -475,7 +481,7 @@ export const getSecrets = async (req: Request, res: Response) => { secretPath = getFolderWithPathFromId(folder.nodes, folderId).folderPath; } - const permissionCheckFn = await checkSecretsPermission({ + const { authVerifier: permissionCheckFn } = await checkSecretsPermission({ authData: req.authData, workspaceId, environment, @@ -529,7 +535,7 @@ export const getSecretByName = async (req: Request, res: Response) => { query: { secretPath, environment, workspaceId, type, include_imports }, params: { secretName } } = await validateRequest(reqValidator.GetSecretByNameV3, req); - + await checkSecretsPermission({ authData: req.authData, workspaceId, @@ -579,8 +585,8 @@ export const createSecret = async (req: Request, res: Response) => { }, params: { secretName } } = await validateRequest(reqValidator.CreateSecretV3, req); - - await checkSecretsPermission({ + + const { membership } = await checkSecretsPermission({ authData: req.authData, workspaceId, environment, @@ -588,6 +594,38 @@ export const createSecret = async (req: Request, res: Response) => { secretAction: ProjectPermissionActions.Create }); + if (membership && type !== "personal") { + const secretApprovalPolicy = await getSecretPolicyOfBoard(workspaceId, environment, secretPath); + if (secretApprovalPolicy) { + const secretApprovalRequest = await generateSecretApprovalRequest({ + workspaceId, + environment, + secretPath, + policy: secretApprovalPolicy, + commiterMembershipId: membership._id.toString(), + authData: req.authData, + data: { + [CommitType.CREATE]: [ + { + secretName, + secretValueCiphertext, + secretValueIV, + secretValueTag, + secretCommentIV, + secretCommentTag, + secretCommentCiphertext, + skipMultilineEncoding, + secretKeyTag, + secretKeyCiphertext, + secretKeyIV + } + ] + } + }); + return res.send({ approval: secretApprovalRequest }); + } + } + const secret = await SecretService.createSecret({ secretName, workspaceId: new Types.ObjectId(workspaceId), @@ -651,12 +689,12 @@ export const updateSecretByName = async (req: Request, res: Response) => { }, params: { secretName } } = await validateRequest(reqValidator.UpdateSecretByNameV3, req); - + if (newSecretName && (!secretKeyIV || !secretKeyTag || !secretKeyCiphertext)) { throw BadRequestError({ message: "Missing encrypted key" }); } - await checkSecretsPermission({ + const { membership } = await checkSecretsPermission({ authData: req.authData, workspaceId, environment, @@ -664,6 +702,40 @@ export const updateSecretByName = async (req: Request, res: Response) => { secretAction: ProjectPermissionActions.Edit }); + if (membership && type !== "personal") { + const secretApprovalPolicy = await getSecretPolicyOfBoard(workspaceId, environment, secretPath); + if (secretApprovalPolicy) { + const secretApprovalRequest = await generateSecretApprovalRequest({ + workspaceId, + environment, + secretPath, + policy: secretApprovalPolicy, + commiterMembershipId: membership._id.toString(), + authData: req.authData, + data: { + [CommitType.UPDATE]: [ + { + secretName, + newSecretName, + secretValueCiphertext, + secretValueIV, + secretValueTag, + tags, + secretCommentIV, + secretCommentTag, + secretCommentCiphertext, + skipMultilineEncoding, + secretKeyTag, + secretKeyCiphertext, + secretKeyIV + } + ] + } + }); + return res.send({ approval: secretApprovalRequest }); + } + } + const secret = await SecretService.updateSecret({ secretName, workspaceId: new Types.ObjectId(workspaceId), @@ -709,7 +781,7 @@ export const deleteSecretByName = async (req: Request, res: Response) => { params: { secretName } } = await validateRequest(reqValidator.DeleteSecretByNameV3, req); - await checkSecretsPermission({ + const { membership } = await checkSecretsPermission({ authData: req.authData, workspaceId, environment, @@ -717,6 +789,28 @@ export const deleteSecretByName = async (req: Request, res: Response) => { secretAction: ProjectPermissionActions.Delete }); + if (membership && type !== "personal") { + const secretApprovalPolicy = await getSecretPolicyOfBoard(workspaceId, environment, secretPath); + if (secretApprovalPolicy) { + const secretApprovalRequest = await generateSecretApprovalRequest({ + workspaceId, + environment, + secretPath, + authData: req.authData, + policy: secretApprovalPolicy, + commiterMembershipId: membership._id.toString(), + data: { + [CommitType.DELETE]: [ + { + secretName + } + ] + } + }); + return res.send({ approval: secretApprovalRequest }); + } + } + const { secret } = await SecretService.deleteSecret({ secretName, workspaceId: new Types.ObjectId(workspaceId), @@ -743,8 +837,8 @@ export const createSecretByNameBatch = async (req: Request, res: Response) => { const { body: { secrets, secretPath, environment, workspaceId } } = await validateRequest(reqValidator.CreateSecretByNameBatchV3, req); - - await checkSecretsPermission({ + + const { membership } = await checkSecretsPermission({ authData: req.authData, workspaceId, environment, @@ -752,6 +846,24 @@ export const createSecretByNameBatch = async (req: Request, res: Response) => { secretAction: ProjectPermissionActions.Create }); + if (membership) { + const secretApprovalPolicy = await getSecretPolicyOfBoard(workspaceId, environment, secretPath); + if (secretApprovalPolicy) { + const secretApprovalRequest = await generateSecretApprovalRequest({ + workspaceId, + environment, + secretPath, + authData: req.authData, + policy: secretApprovalPolicy, + commiterMembershipId: membership._id.toString(), + data: { + [CommitType.CREATE]: secrets.filter(({ type }) => type === "shared") + } + }); + return res.send({ approval: secretApprovalRequest }); + } + } + const createdSecrets = await SecretService.createSecretBatch({ secretPath, environment, @@ -770,7 +882,7 @@ export const updateSecretByNameBatch = async (req: Request, res: Response) => { body: { secrets, secretPath, environment, workspaceId } } = await validateRequest(reqValidator.UpdateSecretByNameBatchV3, req); - await checkSecretsPermission({ + const { membership } = await checkSecretsPermission({ authData: req.authData, workspaceId, environment, @@ -778,6 +890,24 @@ export const updateSecretByNameBatch = async (req: Request, res: Response) => { secretAction: ProjectPermissionActions.Edit }); + if (membership) { + const secretApprovalPolicy = await getSecretPolicyOfBoard(workspaceId, environment, secretPath); + if (secretApprovalPolicy) { + const secretApprovalRequest = await generateSecretApprovalRequest({ + workspaceId, + environment, + secretPath, + policy: secretApprovalPolicy, + commiterMembershipId: membership._id.toString(), + data: { + [CommitType.UPDATE]: secrets.filter(({ type }) => type === "shared") + }, + authData: req.authData + }); + return res.send({ approval: secretApprovalRequest }); + } + } + const updatedSecrets = await SecretService.updateSecretBatch({ secretPath, environment, @@ -796,7 +926,7 @@ export const deleteSecretByNameBatch = async (req: Request, res: Response) => { body: { secrets, secretPath, environment, workspaceId } } = await validateRequest(reqValidator.DeleteSecretByNameBatchV3, req); - await checkSecretsPermission({ + const { membership } = await checkSecretsPermission({ authData: req.authData, workspaceId, environment, @@ -804,6 +934,24 @@ export const deleteSecretByNameBatch = async (req: Request, res: Response) => { secretAction: ProjectPermissionActions.Delete }); + if (membership) { + const secretApprovalPolicy = await getSecretPolicyOfBoard(workspaceId, environment, secretPath); + if (secretApprovalPolicy) { + const secretApprovalRequest = await generateSecretApprovalRequest({ + workspaceId, + environment, + secretPath, + policy: secretApprovalPolicy, + commiterMembershipId: membership._id.toString(), + data: { + [CommitType.DELETE]: secrets.filter(({ type }) => type === "shared") + }, + authData: req.authData + }); + return res.send({ approval: secretApprovalRequest }); + } + } + const deletedSecrets = await SecretService.deleteSecretBatch({ secretPath, environment, @@ -815,4 +963,4 @@ export const deleteSecretByNameBatch = async (req: Request, res: Response) => { return res.status(200).send({ secrets: deletedSecrets }); -}; \ No newline at end of file +}; diff --git a/backend/src/ee/controllers/v1/index.ts b/backend/src/ee/controllers/v1/index.ts index b57a07c9d..b023599e7 100644 --- a/backend/src/ee/controllers/v1/index.ts +++ b/backend/src/ee/controllers/v1/index.ts @@ -8,6 +8,8 @@ import * as actionController from "./actionController"; import * as membershipController from "./membershipController"; import * as cloudProductsController from "./cloudProductsController"; import * as roleController from "./roleController"; +import * as secretApprovalPolicyController from "./secretApprovalPolicyController"; +import * as secretApprovalRequestController from "./secretApprovalRequestsController"; export { secretController, @@ -19,5 +21,7 @@ export { actionController, membershipController, cloudProductsController, - roleController + roleController, + secretApprovalPolicyController, + secretApprovalRequestController }; diff --git a/backend/src/controllers/v1/secretApprovalPolicyController.ts b/backend/src/ee/controllers/v1/secretApprovalPolicyController.ts similarity index 72% rename from backend/src/controllers/v1/secretApprovalPolicyController.ts rename to backend/src/ee/controllers/v1/secretApprovalPolicyController.ts index 245932353..eb2f48718 100644 --- a/backend/src/controllers/v1/secretApprovalPolicyController.ts +++ b/backend/src/ee/controllers/v1/secretApprovalPolicyController.ts @@ -1,20 +1,22 @@ -import { ForbiddenError } from "@casl/ability"; +import { ForbiddenError, subject } from "@casl/ability"; import { Request, Response } from "express"; +import { nanoid } from "nanoid"; import { ProjectPermissionActions, ProjectPermissionSub, getUserProjectPermissions -} from "../../ee/services/ProjectRoleService"; -import { validateRequest } from "../../helpers/validation"; +} from "../../services/ProjectRoleService"; +import { validateRequest } from "../../../helpers/validation"; import { SecretApprovalPolicy } from "../../models/secretApprovalPolicy"; -import { BadRequestError } from "../../utils/errors"; +import { getSecretPolicyOfBoard } from "../../services/SecretApprovalService"; +import { BadRequestError } from "../../../utils/errors"; import * as reqValidator from "../../validation/secretApproval"; const ERR_SECRET_APPROVAL_NOT_FOUND = BadRequestError({ message: "secret approval not found" }); export const createSecretApprovalPolicy = async (req: Request, res: Response) => { const { - body: { approvals, secretPath, approvers, environment, workspaceId } + body: { approvals, secretPath, approvers, environment, workspaceId, name } } = await validateRequest(reqValidator.CreateSecretApprovalRule, req); const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); @@ -25,6 +27,7 @@ export const createSecretApprovalPolicy = async (req: Request, res: Response) => const secretApproval = new SecretApprovalPolicy({ workspace: workspaceId, + name: name ?? `${environment}-${nanoid(3)}`, secretPath, environment, approvals, @@ -39,7 +42,7 @@ export const createSecretApprovalPolicy = async (req: Request, res: Response) => export const updateSecretApprovalPolicy = async (req: Request, res: Response) => { const { - body: { approvals, approvers, secretPath }, + body: { approvals, approvers, secretPath, name }, params: { id } } = await validateRequest(reqValidator.UpdateSecretApprovalRule, req); @@ -58,6 +61,7 @@ export const updateSecretApprovalPolicy = async (req: Request, res: Response) => const updatedDoc = await SecretApprovalPolicy.findByIdAndUpdate(id, { approvals, approvers, + name: (name || secretApproval?.name) ?? `${secretApproval.environment}-${nanoid(3)}`, ...(secretPath === null ? { $unset: { secretPath: 1 } } : { secretPath }) }); @@ -107,3 +111,18 @@ export const getSecretApprovalPolicy = async (req: Request, res: Response) => { approvals: doc }); }; + +export const getSecretApprovalPolicyOfBoard = async (req: Request, res: Response) => { + const { + query: { workspaceId, environment, secretPath } + } = await validateRequest(reqValidator.GetSecretApprovalPolicyOfABoard, req); + + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.Secrets, { secretPath, environment }) + ); + + const secretApprovalPolicy = await getSecretPolicyOfBoard(workspaceId, environment, secretPath); + return res.send({ policy: secretApprovalPolicy }); +}; diff --git a/backend/src/ee/controllers/v1/secretApprovalRequestsController.ts b/backend/src/ee/controllers/v1/secretApprovalRequestsController.ts new file mode 100644 index 000000000..be2e59202 --- /dev/null +++ b/backend/src/ee/controllers/v1/secretApprovalRequestsController.ts @@ -0,0 +1,333 @@ +import { Request, Response } from "express"; +import { getUserProjectPermissions } from "../../services/ProjectRoleService"; +import { validateRequest } from "../../../helpers/validation"; +import { Folder } from "../../../models"; +import { ApprovalStatus, SecretApprovalRequest } from "../../models/secretApprovalRequest"; +import * as reqValidator from "../../validation/secretApprovalRequest"; +import { getFolderWithPathFromId } from "../../../services/FolderService"; +import { BadRequestError, UnauthorizedRequestError } from "../../../utils/errors"; +import { ISecretApprovalPolicy, SecretApprovalPolicy } from "../../models/secretApprovalPolicy"; +import { performSecretApprovalRequestMerge } from "../../services/SecretApprovalService"; +import { Types } from "mongoose"; +import { EEAuditLogService } from "../../services"; +import { EventType } from "../../models"; + +export const getSecretApprovalRequestCount = async (req: Request, res: Response) => { + const { + query: { workspaceId } + } = await validateRequest(reqValidator.getSecretApprovalRequestCount, req); + + const { membership } = await getUserProjectPermissions(req.user._id, workspaceId); + const approvalRequestCount = await SecretApprovalRequest.aggregate([ + { + $match: { + workspace: new Types.ObjectId(workspaceId) + } + }, + { + $lookup: { + from: SecretApprovalPolicy.collection.name, + localField: "policy", + foreignField: "_id", + as: "policy" + } + }, + { $unwind: "$policy" }, + ...(membership.role !== "admin" + ? [ + { + $match: { + $or: [ + { committer: new Types.ObjectId(membership.id) }, + { "policy.approvers": new Types.ObjectId(membership.id) } + ] + } + } + ] + : []), + { + $group: { + _id: "$status", + count: { $sum: 1 } + } + } + ]); + const openRequests = approvalRequestCount.find(({ _id }) => _id === "open"); + const closedRequests = approvalRequestCount.find(({ _id }) => _id === "close"); + + return res.send({ + approvals: { open: openRequests?.count || 0, closed: closedRequests?.count || 0 } + }); +}; + +export const getSecretApprovalRequests = async (req: Request, res: Response) => { + const { + query: { status, committer, workspaceId, environment, limit, offset } + } = await validateRequest(reqValidator.getSecretApprovalRequests, req); + + const { membership } = await getUserProjectPermissions(req.user._id, workspaceId); + + const query = { + workspace: new Types.ObjectId(workspaceId), + environment, + committer: committer ? new Types.ObjectId(committer) : undefined, + status + }; + // to strip of undefined in query we use es6 spread to ignore those fields + Object.entries(query).forEach( + ([key, value]) => value === undefined && delete query[key as keyof typeof query] + ); + const approvalRequests = await SecretApprovalRequest.aggregate([ + { + $match: query + }, + { $sort: { createdAt: -1 } }, + { + $lookup: { + from: SecretApprovalPolicy.collection.name, + localField: "policy", + foreignField: "_id", + as: "policy" + } + }, + { $unwind: "$policy" }, + ...(membership.role !== "admin" + ? [ + { + $match: { + $or: [ + { committer: new Types.ObjectId(membership.id) }, + { "policy.approvers": new Types.ObjectId(membership.id) } + ] + } + } + ] + : []), + { $skip: offset }, + { $limit: limit } + ]); + if (!approvalRequests.length) return res.send({ approvals: [] }); + + const unqiueEnvs = environment ?? { + $in: [...new Set(approvalRequests.map(({ environment }) => environment))] + }; + const approvalRootFolders = await Folder.find({ + workspace: workspaceId, + environment: unqiueEnvs + }).lean(); + + const formatedApprovals = approvalRequests.map((el) => { + let secretPath = "/"; + const folders = approvalRootFolders.find(({ environment }) => environment === el.environment); + if (folders) { + secretPath = getFolderWithPathFromId(folders?.nodes, el.folderId)?.folderPath || "/"; + } + return { ...el, secretPath }; + }); + + return res.send({ + approvals: formatedApprovals + }); +}; + +export const getSecretApprovalRequestDetails = async (req: Request, res: Response) => { + const { + params: { id } + } = await validateRequest(reqValidator.getSecretApprovalRequestDetails, req); + const secretApprovalRequest = await SecretApprovalRequest.findById(id) + .populate<{ policy: ISecretApprovalPolicy }>("policy") + .populate({ + path: "commits.secretVersion", + populate: { + path: "tags" + } + }) + .populate("commits.secret", "version") + .populate("commits.newVersion.tags") + .lean(); + if (!secretApprovalRequest) + throw BadRequestError({ message: "Secret approval request not found" }); + + const { membership } = await getUserProjectPermissions( + req.user._id, + secretApprovalRequest.workspace.toString() + ); + // allow to fetch only if its admin or is the committer or approver + if ( + membership.role !== "admin" && + secretApprovalRequest.committer !== membership.id && + !secretApprovalRequest.policy.approvers.find( + (approverId) => approverId.toString() === membership._id.toString() + ) + ) { + throw UnauthorizedRequestError({ message: "User has no access" }); + } + + let secretPath = "/"; + const approvalRootFolders = await Folder.findOne({ + workspace: secretApprovalRequest.workspace, + environment: secretApprovalRequest.environment + }).lean(); + if (approvalRootFolders) { + secretPath = + getFolderWithPathFromId(approvalRootFolders?.nodes, secretApprovalRequest.folderId) + ?.folderPath || "/"; + } + + return res.send({ + approval: { ...secretApprovalRequest, secretPath } + }); +}; + +export const updateSecretApprovalReviewStatus = async (req: Request, res: Response) => { + const { + body: { status }, + params: { id } + } = await validateRequest(reqValidator.updateSecretApprovalReviewStatus, req); + const secretApprovalRequest = await SecretApprovalRequest.findById(id).populate<{ + policy: ISecretApprovalPolicy; + }>("policy"); + if (!secretApprovalRequest) + throw BadRequestError({ message: "Secret approval request not found" }); + + const { membership } = await getUserProjectPermissions( + req.user._id, + secretApprovalRequest.workspace.toString() + ); + if ( + membership.role !== "admin" && + secretApprovalRequest.committer !== membership.id && + !secretApprovalRequest.policy.approvers.find((approverId) => approverId.equals(membership.id)) + ) { + throw UnauthorizedRequestError({ message: "User has no access" }); + } + + const reviewerPos = secretApprovalRequest.reviewers.findIndex( + ({ member }) => member.toString() === membership._id.toString() + ); + if (reviewerPos !== -1) { + secretApprovalRequest.reviewers[reviewerPos].status = status; + } else { + secretApprovalRequest.reviewers.push({ member: membership._id, status }); + } + await secretApprovalRequest.save(); + + return res.send({ status }); +}; + +export const mergeSecretApprovalRequest = async (req: Request, res: Response) => { + const { + params: { id } + } = await validateRequest(reqValidator.mergeSecretApprovalRequest, req); + + const secretApprovalRequest = await SecretApprovalRequest.findById(id).populate<{ + policy: ISecretApprovalPolicy; + }>("policy"); + + if (!secretApprovalRequest) + throw BadRequestError({ message: "Secret approval request not found" }); + + const { membership } = await getUserProjectPermissions( + req.user._id, + secretApprovalRequest.workspace.toString() + ); + if ( + membership.role !== "admin" && + secretApprovalRequest.committer !== membership.id && + !secretApprovalRequest.policy.approvers.find((approverId) => approverId.equals(membership.id)) + ) { + throw UnauthorizedRequestError({ message: "User has no access" }); + } + + const reviewers = secretApprovalRequest.reviewers.reduce>( + (prev, curr) => ({ ...prev, [curr.member.toString()]: curr.status }), + {} + ); + const hasMinApproval = + secretApprovalRequest.policy.approvals <= + secretApprovalRequest.policy.approvers.filter( + (approverId) => reviewers[approverId.toString()] === ApprovalStatus.APPROVED + ).length; + + if (!hasMinApproval) throw BadRequestError({ message: "Doesn't have minimum approvals needed" }); + + const approval = await performSecretApprovalRequestMerge( + id, + req.authData, + membership._id.toString() + ); + return res.send({ approval }); +}; + +export const updateSecretApprovalRequestStatus = async (req: Request, res: Response) => { + const { + body: { status }, + params: { id } + } = await validateRequest(reqValidator.updateSecretApprovalRequestStatus, req); + + const secretApprovalRequest = await SecretApprovalRequest.findById(id).populate<{ + policy: ISecretApprovalPolicy; + }>("policy"); + + if (!secretApprovalRequest) + throw BadRequestError({ message: "Secret approval request not found" }); + + const { membership } = await getUserProjectPermissions( + req.user._id, + secretApprovalRequest.workspace.toString() + ); + + if ( + membership.role !== "admin" && + secretApprovalRequest.committer !== membership.id && + !secretApprovalRequest.policy.approvers.find((approverId) => approverId.equals(membership._id)) + ) { + throw UnauthorizedRequestError({ message: "User has no access" }); + } + + if (secretApprovalRequest.hasMerged) + throw BadRequestError({ message: "Approval request has been merged" }); + if (secretApprovalRequest.status === "close" && status === "close") + throw BadRequestError({ message: "Approval request is already closed" }); + if (secretApprovalRequest.status === "open" && status === "open") + throw BadRequestError({ message: "Approval request is already open" }); + + const updatedRequest = await SecretApprovalRequest.findByIdAndUpdate( + id, + { status, statusChangeBy: membership._id }, + { new: true } + ); + + if (status === "close") { + await EEAuditLogService.createAuditLog( + req.authData, + { + type: EventType.SECRET_APPROVAL_CLOSED, + metadata: { + closedBy: membership._id.toString(), + secretApprovalRequestId: id, + secretApprovalRequestSlug: secretApprovalRequest.slug + } + }, + { + workspaceId: secretApprovalRequest.workspace + } + ); + } else { + await EEAuditLogService.createAuditLog( + req.authData, + { + type: EventType.SECRET_APPROVAL_REOPENED, + metadata: { + reopenedBy: membership._id.toString(), + secretApprovalRequestId: id, + secretApprovalRequestSlug: secretApprovalRequest.slug + } + }, + { + workspaceId: secretApprovalRequest.workspace + } + ); + } + return res.send({ approval: updatedRequest }); +}; diff --git a/backend/src/ee/models/auditLog/enums.ts b/backend/src/ee/models/auditLog/enums.ts index 3111db0af..a7be37a65 100644 --- a/backend/src/ee/models/auditLog/enums.ts +++ b/backend/src/ee/models/auditLog/enums.ts @@ -12,43 +12,47 @@ export enum UserAgentType { } export enum EventType { - GET_SECRETS = "get-secrets", - GET_SECRET = "get-secret", - REVEAL_SECRET = "reveal-secret", - CREATE_SECRET = "create-secret", - CREATE_SECRETS = "create-secrets", - UPDATE_SECRET = "update-secret", - UPDATE_SECRETS = "update-secrets", - DELETE_SECRET = "delete-secret", - DELETE_SECRETS = "delete-secrets", - GET_WORKSPACE_KEY = "get-workspace-key", - AUTHORIZE_INTEGRATION = "authorize-integration", - UNAUTHORIZE_INTEGRATION = "unauthorize-integration", - CREATE_INTEGRATION = "create-integration", - DELETE_INTEGRATION = "delete-integration", - ADD_TRUSTED_IP = "add-trusted-ip", - UPDATE_TRUSTED_IP = "update-trusted-ip", - DELETE_TRUSTED_IP = "delete-trusted-ip", - CREATE_SERVICE_TOKEN = "create-service-token", // v2 - DELETE_SERVICE_TOKEN = "delete-service-token", // v2 - CREATE_SERVICE_TOKEN_V3 = "create-service-token-v3", // v3 - UPDATE_SERVICE_TOKEN_V3 = "update-service-token-v3", // v3 - DELETE_SERVICE_TOKEN_V3 = "delete-service-token-v3", // v3 - CREATE_ENVIRONMENT = "create-environment", - UPDATE_ENVIRONMENT = "update-environment", - DELETE_ENVIRONMENT = "delete-environment", - ADD_WORKSPACE_MEMBER = "add-workspace-member", - REMOVE_WORKSPACE_MEMBER = "remove-workspace-member", - CREATE_FOLDER = "create-folder", - UPDATE_FOLDER = "update-folder", - DELETE_FOLDER = "delete-folder", - CREATE_WEBHOOK = "create-webhook", - UPDATE_WEBHOOK_STATUS = "update-webhook-status", - DELETE_WEBHOOK = "delete-webhook", - GET_SECRET_IMPORTS = "get-secret-imports", - CREATE_SECRET_IMPORT = "create-secret-import", - UPDATE_SECRET_IMPORT = "update-secret-import", - DELETE_SECRET_IMPORT = "delete-secret-import", - UPDATE_USER_WORKSPACE_ROLE = "update-user-workspace-role", - UPDATE_USER_WORKSPACE_DENIED_PERMISSIONS = "update-user-workspace-denied-permissions" -} \ No newline at end of file + GET_SECRETS = "get-secrets", + GET_SECRET = "get-secret", + REVEAL_SECRET = "reveal-secret", + CREATE_SECRET = "create-secret", + CREATE_SECRETS = "create-secrets", + UPDATE_SECRET = "update-secret", + UPDATE_SECRETS = "update-secrets", + DELETE_SECRET = "delete-secret", + DELETE_SECRETS = "delete-secrets", + GET_WORKSPACE_KEY = "get-workspace-key", + AUTHORIZE_INTEGRATION = "authorize-integration", + UNAUTHORIZE_INTEGRATION = "unauthorize-integration", + CREATE_INTEGRATION = "create-integration", + DELETE_INTEGRATION = "delete-integration", + ADD_TRUSTED_IP = "add-trusted-ip", + UPDATE_TRUSTED_IP = "update-trusted-ip", + DELETE_TRUSTED_IP = "delete-trusted-ip", + CREATE_SERVICE_TOKEN = "create-service-token", // v2 + DELETE_SERVICE_TOKEN = "delete-service-token", // v2 + CREATE_SERVICE_TOKEN_V3 = "create-service-token-v3", // v3 + UPDATE_SERVICE_TOKEN_V3 = "update-service-token-v3", // v3 + DELETE_SERVICE_TOKEN_V3 = "delete-service-token-v3", // v3 + CREATE_ENVIRONMENT = "create-environment", + UPDATE_ENVIRONMENT = "update-environment", + DELETE_ENVIRONMENT = "delete-environment", + ADD_WORKSPACE_MEMBER = "add-workspace-member", + REMOVE_WORKSPACE_MEMBER = "remove-workspace-member", + CREATE_FOLDER = "create-folder", + UPDATE_FOLDER = "update-folder", + DELETE_FOLDER = "delete-folder", + CREATE_WEBHOOK = "create-webhook", + UPDATE_WEBHOOK_STATUS = "update-webhook-status", + DELETE_WEBHOOK = "delete-webhook", + GET_SECRET_IMPORTS = "get-secret-imports", + CREATE_SECRET_IMPORT = "create-secret-import", + UPDATE_SECRET_IMPORT = "update-secret-import", + DELETE_SECRET_IMPORT = "delete-secret-import", + UPDATE_USER_WORKSPACE_ROLE = "update-user-workspace-role", + UPDATE_USER_WORKSPACE_DENIED_PERMISSIONS = "update-user-workspace-denied-permissions", + SECRET_APPROVAL_MERGED = "secret-approval-merged", + SECRET_APPROVAL_REQUEST = "secret-approval-request", + SECRET_APPROVAL_CLOSED = "secret-approval-closed", + SECRET_APPROVAL_REOPENED = "secret-approval-reopened" +} diff --git a/backend/src/ee/models/auditLog/types.ts b/backend/src/ee/models/auditLog/types.ts index 2f9fdb41c..579a02ccb 100644 --- a/backend/src/ee/models/auditLog/types.ts +++ b/backend/src/ee/models/auditLog/types.ts @@ -437,43 +437,82 @@ interface UpdateUserDeniedPermissions { }[] } } +interface SecretApprovalMerge { + type: EventType.SECRET_APPROVAL_MERGED; + metadata: { + mergedBy: string; + secretApprovalRequestSlug: string; + secretApprovalRequestId: string; + }; +} -export type Event = - | GetSecretsEvent - | GetSecretEvent - | CreateSecretEvent - | CreateSecretBatchEvent - | UpdateSecretEvent - | UpdateSecretBatchEvent - | DeleteSecretEvent - | DeleteSecretBatchEvent - | GetWorkspaceKeyEvent - | AuthorizeIntegrationEvent - | UnauthorizeIntegrationEvent - | CreateIntegrationEvent - | DeleteIntegrationEvent - | AddTrustedIPEvent - | UpdateTrustedIPEvent - | DeleteTrustedIPEvent - | CreateServiceTokenEvent - | DeleteServiceTokenEvent - | CreateServiceTokenV3Event - | UpdateServiceTokenV3Event - | DeleteServiceTokenV3Event - | CreateEnvironmentEvent - | UpdateEnvironmentEvent - | DeleteEnvironmentEvent - | AddWorkspaceMemberEvent - | RemoveWorkspaceMemberEvent - | CreateFolderEvent - | UpdateFolderEvent - | DeleteFolderEvent - | CreateWebhookEvent - | UpdateWebhookStatusEvent - | DeleteWebhookEvent - | GetSecretImportsEvent - | CreateSecretImportEvent - | UpdateSecretImportEvent - | DeleteSecretImportEvent - | UpdateUserRole - | UpdateUserDeniedPermissions; \ No newline at end of file +interface SecretApprovalClosed { + type: EventType.SECRET_APPROVAL_CLOSED; + metadata: { + closedBy: string; + secretApprovalRequestSlug: string; + secretApprovalRequestId: string; + }; +} + +interface SecretApprovalReopened { + type: EventType.SECRET_APPROVAL_REOPENED; + metadata: { + reopenedBy: string; + secretApprovalRequestSlug: string; + secretApprovalRequestId: string; + }; +} + +interface SecretApprovalRequest { + type: EventType.SECRET_APPROVAL_REQUEST; + metadata: { + committedBy: string; + secretApprovalRequestSlug: string; + secretApprovalRequestId: string; + }; +} + +export type Event = + | GetSecretsEvent + | GetSecretEvent + | CreateSecretEvent + | CreateSecretBatchEvent + | UpdateSecretEvent + | UpdateSecretBatchEvent + | DeleteSecretEvent + | DeleteSecretBatchEvent + | GetWorkspaceKeyEvent + | AuthorizeIntegrationEvent + | UnauthorizeIntegrationEvent + | CreateIntegrationEvent + | DeleteIntegrationEvent + | AddTrustedIPEvent + | UpdateTrustedIPEvent + | DeleteTrustedIPEvent + | CreateServiceTokenEvent + | DeleteServiceTokenEvent + | CreateServiceTokenV3Event + | UpdateServiceTokenV3Event + | DeleteServiceTokenV3Event + | CreateEnvironmentEvent + | UpdateEnvironmentEvent + | DeleteEnvironmentEvent + | AddWorkspaceMemberEvent + | RemoveWorkspaceMemberEvent + | CreateFolderEvent + | UpdateFolderEvent + | DeleteFolderEvent + | CreateWebhookEvent + | UpdateWebhookStatusEvent + | DeleteWebhookEvent + | GetSecretImportsEvent + | CreateSecretImportEvent + | UpdateSecretImportEvent + | DeleteSecretImportEvent + | UpdateUserRole + | UpdateUserDeniedPermissions + | SecretApprovalMerge + | SecretApprovalClosed + | SecretApprovalRequest + | SecretApprovalReopened; diff --git a/backend/src/models/secretApprovalPolicy.ts b/backend/src/ee/models/secretApprovalPolicy.ts similarity index 94% rename from backend/src/models/secretApprovalPolicy.ts rename to backend/src/ee/models/secretApprovalPolicy.ts index 9471f82db..376b541c7 100644 --- a/backend/src/models/secretApprovalPolicy.ts +++ b/backend/src/ee/models/secretApprovalPolicy.ts @@ -3,6 +3,7 @@ import { Schema, Types, model } from "mongoose"; export interface ISecretApprovalPolicy { _id: Types.ObjectId; workspace: Types.ObjectId; + name: string; environment: string; secretPath?: string; approvers: Types.ObjectId[]; @@ -23,6 +24,9 @@ const secretApprovalPolicySchema = new Schema( ref: "Membership" } ], + name: { + type: String + }, environment: { type: String, required: true diff --git a/backend/src/ee/models/secretApprovalRequest.ts b/backend/src/ee/models/secretApprovalRequest.ts new file mode 100644 index 000000000..24e8af39a --- /dev/null +++ b/backend/src/ee/models/secretApprovalRequest.ts @@ -0,0 +1,203 @@ +import { Schema, Types, model } from "mongoose"; +import { customAlphabet } from "nanoid"; +import { + ALGORITHM_AES_256_GCM, + ENCODING_SCHEME_BASE64, + ENCODING_SCHEME_UTF8 +} from "../../variables"; + +export enum ApprovalStatus { + PENDING = "pending", + APPROVED = "approved", + REJECTED = "rejected" +} + +export enum CommitType { + DELETE = "delete", + UPDATE = "update", + CREATE = "create" +} + +const SLUG_ALPHABETS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; +const nanoId = customAlphabet(SLUG_ALPHABETS, 10); + +export interface ISecretApprovalSecChange { + _id: Types.ObjectId; + version: number; + secretBlindIndex?: string; + secretKeyCiphertext: string; + secretKeyIV: string; + secretKeyTag: string; + secretValueCiphertext: string; + secretValueIV: string; + secretValueTag: string; + secretCommentIV?: string; + secretCommentTag?: string; + secretCommentCiphertext?: string; + skipMultilineEncoding?: boolean; + algorithm?: "aes-256-gcm"; + keyEncoding?: "utf8" | "base64"; + tags?: string[]; +} + +export type ISecretCommits = Array< + | { + newVersion: ISecretApprovalSecChange; + op: CommitType.CREATE; + } + | { + // secret is recorded to get the latest version, we can keep ref to secret for pulling change as it will also get changed + // on merge + secretVersion: J; + secret: T; + newVersion: Partial> & { _id: Types.ObjectId }; + op: CommitType.UPDATE; + } + | { + secret: T; + secretVersion: J; + op: CommitType.DELETE; + } +>; +export interface ISecretApprovalRequest { + _id: Types.ObjectId; + committer: Types.ObjectId; + slug: string; + statusChangeBy: Types.ObjectId; + reviewers: { + member: Types.ObjectId; + status: ApprovalStatus; + }[]; + workspace: Types.ObjectId; + environment: string; + folderId: string; + hasMerged: boolean; + status: "open" | "close"; + policy: Types.ObjectId; + commits: ISecretCommits; + conflicts: Array<{ secretId: string; op: CommitType }>; +} + +const secretApprovalSecretChangeSchema = new Schema({ + version: { + type: Number, + default: 1, + required: true + }, + secretBlindIndex: { + type: String, + select: false + }, + secretKeyCiphertext: { + type: String, + required: true + }, + secretKeyIV: { + type: String, // symmetric + required: true + }, + secretKeyTag: { + type: String, // symmetric + required: true + }, + secretValueCiphertext: { + type: String, + required: true + }, + secretValueIV: { + type: String, // symmetric + required: true + }, + secretValueTag: { + type: String, // symmetric + required: true + }, + skipMultilineEncoding: { + type: Boolean, + required: false + }, + algorithm: { + // the encryption algorithm used + type: String, + enum: [ALGORITHM_AES_256_GCM], + required: true, + default: ALGORITHM_AES_256_GCM + }, + keyEncoding: { + type: String, + enum: [ENCODING_SCHEME_UTF8, ENCODING_SCHEME_BASE64], + required: true, + default: ENCODING_SCHEME_UTF8 + }, + tags: { + ref: "Tag", + type: [Schema.Types.ObjectId], + default: [] + } +}); + +const secretApprovalRequestSchema = new Schema( + { + workspace: { + type: Schema.Types.ObjectId, + ref: "Workspace", + required: true + }, + environment: { + type: String, + required: true + }, + folderId: { + type: String, + required: true, + default: "root" + }, + slug: { + type: String, + default: () => nanoId() + }, + reviewers: { + type: [ + { + member: { + // user associated with the personal secret + type: Schema.Types.ObjectId, + ref: "Membership" + }, + status: { type: String, enum: ApprovalStatus, default: ApprovalStatus.PENDING } + } + ], + default: [] + }, + policy: { type: Schema.Types.ObjectId, ref: "SecretApprovalPolicy" }, + hasMerged: { type: Boolean, default: false }, + status: { type: String, enum: ["close", "open"], default: "open" }, + committer: { type: Schema.Types.ObjectId, ref: "Membership" }, + statusChangeBy: { type: Schema.Types.ObjectId, ref: "Membership" }, + commits: [ + { + secret: { type: Types.ObjectId, ref: "Secret" }, + newVersion: secretApprovalSecretChangeSchema, + secretVersion: { type: Types.ObjectId, ref: "SecretVersion" }, + op: { type: String, enum: [CommitType], required: true } + } + ], + conflicts: { + type: [ + { + secretId: { type: String, required: true }, + op: { type: String, enum: [CommitType], required: true } + } + ], + default: [] + } + }, + { + timestamps: true + } +); + +export const SecretApprovalRequest = model( + "SecretApprovalRequest", + secretApprovalRequestSchema +); diff --git a/backend/src/ee/routes/v1/index.ts b/backend/src/ee/routes/v1/index.ts index 9b8d1bea3..34477cf72 100644 --- a/backend/src/ee/routes/v1/index.ts +++ b/backend/src/ee/routes/v1/index.ts @@ -8,6 +8,8 @@ import action from "./action"; import cloudProducts from "./cloudProducts"; import secretScanning from "./secretScanning"; import roles from "./role"; +import secretApprovalPolicy from "./secretApprovalPolicy"; +import secretApprovalRequest from "./secretApprovalRequest"; export { secret, @@ -19,5 +21,7 @@ export { action, cloudProducts, secretScanning, - roles + roles, + secretApprovalPolicy, + secretApprovalRequest }; diff --git a/backend/src/routes/v1/secretApprovalPolicy.ts b/backend/src/ee/routes/v1/secretApprovalPolicy.ts similarity index 74% rename from backend/src/routes/v1/secretApprovalPolicy.ts rename to backend/src/ee/routes/v1/secretApprovalPolicy.ts index 51c2aa3e7..c4b082286 100644 --- a/backend/src/routes/v1/secretApprovalPolicy.ts +++ b/backend/src/ee/routes/v1/secretApprovalPolicy.ts @@ -1,8 +1,8 @@ import express from "express"; const router = express.Router(); -import { requireAuth } from "../../middleware"; +import { requireAuth } from "../../../middleware"; import { secretApprovalPolicyController } from "../../controllers/v1"; -import { AuthMode } from "../../variables"; +import { AuthMode } from "../../../variables"; router.get( "/", @@ -12,6 +12,14 @@ router.get( secretApprovalPolicyController.getSecretApprovalPolicy ); +router.get( + "/board", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + secretApprovalPolicyController.getSecretApprovalPolicyOfBoard +); + router.post( "/", requireAuth({ diff --git a/backend/src/ee/routes/v1/secretApprovalRequest.ts b/backend/src/ee/routes/v1/secretApprovalRequest.ts new file mode 100644 index 000000000..e78c67c96 --- /dev/null +++ b/backend/src/ee/routes/v1/secretApprovalRequest.ts @@ -0,0 +1,55 @@ +import express from "express"; +const router = express.Router(); +import { requireAuth } from "../../../middleware"; +import { secretApprovalRequestController } from "../../controllers/v1"; +import { AuthMode } from "../../../variables"; + +router.get( + "/", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + secretApprovalRequestController.getSecretApprovalRequests +); + +router.get( + "/count", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + secretApprovalRequestController.getSecretApprovalRequestCount +); + +router.get( + "/:id", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + secretApprovalRequestController.getSecretApprovalRequestDetails +); + +router.post( + "/:id/merge", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + secretApprovalRequestController.mergeSecretApprovalRequest +); + +router.post( + "/:id/review", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + secretApprovalRequestController.updateSecretApprovalReviewStatus +); + +router.post( + "/:id/status", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + secretApprovalRequestController.updateSecretApprovalRequestStatus +); + +export default router; diff --git a/backend/src/ee/services/EELicenseService.ts b/backend/src/ee/services/EELicenseService.ts index 733bf80e0..f403f55d6 100644 --- a/backend/src/ee/services/EELicenseService.ts +++ b/backend/src/ee/services/EELicenseService.ts @@ -1,12 +1,12 @@ import { Types } from "mongoose"; import * as Sentry from "@sentry/node"; import NodeCache from "node-cache"; -import { +import { getLicenseKey, getLicenseServerKey, getLicenseServerUrl, } from "../../config"; -import { +import { licenseKeyRequest, licenseServerKeyRequest, refreshLicenseKeyToken, @@ -37,6 +37,7 @@ interface FeatureSet { status: "incomplete" | "incomplete_expired" | "trialing" | "active" | "past_due" | "canceled" | "unpaid" | null; trial_end: number | null; has_used_trial: boolean; + secretApproval: boolean; } /** @@ -46,7 +47,7 @@ interface FeatureSet { * - Self-hosted enterprise: Fetch and update global feature set */ class EELicenseService { - + private readonly _isLicenseValid: boolean; // TODO: deprecate public instanceType: "self-hosted" | "enterprise-self-hosted" | "cloud" = "self-hosted"; @@ -72,18 +73,19 @@ class EELicenseService { samlSSO: false, status: null, trial_end: null, - has_used_trial: true + has_used_trial: true, + secretApproval: false } public localFeatureSet: NodeCache; - + constructor() { this._isLicenseValid = true; this.localFeatureSet = new NodeCache({ stdTTL: 60, }); } - + public async getPlan(organizationId: Types.ObjectId, workspaceId?: Types.ObjectId): Promise { try { if (this.instanceType === "cloud") { @@ -96,7 +98,7 @@ class EELicenseService { if (!organization) throw OrganizationNotFoundError(); let url = `${await getLicenseServerUrl()}/api/license-server/v1/customers/${organization.customerId}/cloud-plan`; - + if (workspaceId) { url += `?workspaceId=${workspaceId}`; } @@ -114,14 +116,14 @@ class EELicenseService { return this.globalFeatureSet; } - + public async refreshPlan(organizationId: Types.ObjectId, workspaceId?: Types.ObjectId) { if (this.instanceType === "cloud") { this.localFeatureSet.del(`${organizationId.toString()}-${workspaceId?.toString() ?? ""}`); await this.getPlan(organizationId, workspaceId); } } - + public async delPlan(organizationId: Types.ObjectId) { if (this.instanceType === "cloud") { this.localFeatureSet.del(`${organizationId.toString()}-`); @@ -136,23 +138,23 @@ class EELicenseService { if (licenseServerKey) { // license server key is present -> validate it const token = await refreshLicenseServerKeyToken() - + if (token) { this.instanceType = "cloud"; } - + return; } - + if (licenseKey) { // license key is present -> validate it const token = await refreshLicenseKeyToken(); - + if (token) { const { data: { currentPlan } } = await licenseKeyRequest.get( `${await getLicenseServerUrl()}/api/license/v1/plan` ); - + this.globalFeatureSet = currentPlan; this.instanceType = "enterprise-self-hosted"; } diff --git a/backend/src/ee/services/SecretApprovalService.ts b/backend/src/ee/services/SecretApprovalService.ts new file mode 100644 index 000000000..58e2a9a20 --- /dev/null +++ b/backend/src/ee/services/SecretApprovalService.ts @@ -0,0 +1,656 @@ +import picomatch from "picomatch"; +import { Types } from "mongoose"; +import { + containsGlobPatterns, + generateSecretBlindIndexWithSaltHelper, + getSecretBlindIndexSaltHelper +} from "../../helpers/secrets"; +import { Folder, ISecret, Secret } from "../../models"; +import { ISecretApprovalPolicy, SecretApprovalPolicy } from "../models/secretApprovalPolicy"; +import { + CommitType, + ISecretApprovalRequest, + ISecretApprovalSecChange, + ISecretCommits, + SecretApprovalRequest +} from "../models/secretApprovalRequest"; +import { BadRequestError } from "../../utils/errors"; +import { getFolderByPath } from "../../services/FolderService"; +import { ALGORITHM_AES_256_GCM, ENCODING_SCHEME_UTF8, SECRET_SHARED } from "../../variables"; +import TelemetryService from "../../services/TelemetryService"; +import { EEAuditLogService, EESecretService } from "../services"; +import { EventType, SecretVersion } from "../models"; +import { AuthData } from "../../interfaces/middleware"; + +// if glob pattern score is 1, if not exist score is 0 and if its not both then its exact path meaning score 2 +const getPolicyScore = (policy: ISecretApprovalPolicy) => + policy.secretPath ? (containsGlobPatterns(policy.secretPath) ? 1 : 2) : 0; + +// this will fetch the policy that gets priority for an environment and secret path +export const getSecretPolicyOfBoard = async ( + workspaceId: string, + environment: string, + secretPath: string +) => { + const policies = await SecretApprovalPolicy.find({ workspace: workspaceId, environment }); + if (!policies) return; + // this will filter policies either without scoped to secret path or the one that matches with secret path + const policiesFilteredByPath = policies.filter( + ({ secretPath: policyPath }) => + !policyPath || picomatch.isMatch(secretPath, policyPath, { strictSlashes: false }) + ); + // now sort by priority. exact secret path gets first match followed by glob followed by just env scoped + // if that is tie get by first createdAt + const policiesByPriority = policiesFilteredByPath.sort( + (a, b) => getPolicyScore(b) - getPolicyScore(a) + ); + const finalPolicy = policiesByPriority.shift(); + return finalPolicy; +}; + +const getLatestSecretVersion = async (secretIds: Types.ObjectId[]) => { + const latestSecretVersions = await SecretVersion.aggregate([ + { + $match: { + secret: { + $in: secretIds + }, + type: SECRET_SHARED + } + }, + { + $sort: { version: -1 } + }, + { + $group: { + _id: "$secret", + version: { $max: "$version" }, + versionId: { $max: "$_id" }, // id of latest secret versionId + secret: { $first: "$$ROOT" } + } + } + ]).exec(); + // reduced with secret id and latest version as document + return latestSecretVersions.reduce( + (prev, curr) => ({ ...prev, [curr._id.toString()]: curr.secret }), + {} + ); +}; + +type TApprovalCreateSecret = Omit & { + secretName: string; +}; +type TApprovalUpdateSecret = Partial> & { + secretName: string; + newSecretName?: string; +}; + +type TGenerateSecretApprovalRequestArg = { + workspaceId: string; + environment: string; + secretPath: string; + policy: ISecretApprovalPolicy; + data: { + [CommitType.CREATE]?: TApprovalCreateSecret[]; + [CommitType.UPDATE]?: TApprovalUpdateSecret[]; + [CommitType.DELETE]?: { secretName: string }[]; + }; + commiterMembershipId: string; + authData: AuthData; +}; + +export const generateSecretApprovalRequest = async ({ + workspaceId, + environment, + secretPath, + policy, + data, + commiterMembershipId, + authData +}: TGenerateSecretApprovalRequestArg) => { + // calculate folder id from secret path + let folderId = "root"; + const rootFolder = await Folder.findOne({ workspace: workspaceId, environment }); + if (!rootFolder && secretPath !== "/") throw BadRequestError({ message: "Folder not found" }); + if (rootFolder) { + const folder = getFolderByPath(rootFolder.nodes, secretPath); + if (!folder) throw BadRequestError({ message: "Folder not found" }); + folderId = folder.id; + } + + // generate secret blindIndexes + const salt = await getSecretBlindIndexSaltHelper({ + workspaceId: new Types.ObjectId(workspaceId) + }); + const commits: ISecretApprovalRequest["commits"] = []; + + // ----- + // for created secret approval change + const createdSecret = data[CommitType.CREATE]; + if (createdSecret && createdSecret?.length) { + // validation checks whether secret exists for creation + const secretBlindIndexes = await Promise.all( + createdSecret.map(({ secretName }) => + generateSecretBlindIndexWithSaltHelper({ + secretName, + salt + }) + ) + ).then((blindIndexes) => + blindIndexes.reduce>((prev, curr, i) => { + prev[createdSecret[i].secretName] = curr; + return prev; + }, {}) + ); + // check created secret exists + const exists = await Secret.exists({ + workspace: new Types.ObjectId(workspaceId), + folder: folderId, + environment + }) + .or( + createdSecret.map(({ secretName }) => ({ + secretBlindIndex: secretBlindIndexes[secretName], + type: SECRET_SHARED + })) + ) + .exec(); + if (exists) throw BadRequestError({ message: "Secrets already exist" }); + commits.push( + ...createdSecret.map((el) => ({ + op: CommitType.CREATE as const, + newVersion: { + ...el, + version: 0, + _id: new Types.ObjectId(), + secretBlindIndex: secretBlindIndexes[el.secretName] + } + })) + ); + } + + // ---- + // updated secrets approval change + const updatedSecret = data[CommitType.UPDATE]; + if (updatedSecret && updatedSecret?.length) { + // validation checks whether secret doesn't exists for update + const secretBlindIndexes = await Promise.all( + updatedSecret.map(({ secretName }) => + generateSecretBlindIndexWithSaltHelper({ + secretName, + salt + }) + ) + ).then((blindIndexes) => + blindIndexes.reduce>((prev, curr, i) => { + prev[updatedSecret[i].secretName] = curr; + return prev; + }, {}) + ); + // check update secret exists + const oldSecrets = await Secret.find({ + workspace: new Types.ObjectId(workspaceId), + folder: folderId, + environment, + type: SECRET_SHARED, + secretBlindIndex: { + $in: updatedSecret.map(({ secretName }) => secretBlindIndexes[secretName]) + } + }) + .select("+secretBlindIndex") + .lean() + .exec(); + if (oldSecrets.length !== updatedSecret.length) + throw BadRequestError({ message: "Secrets already exist" }); + + // finally check updating blindindex exist + const nameUpdatedSecrets = updatedSecret.filter(({ newSecretName }) => Boolean(newSecretName)); + const newSecretBlindIndexes = await Promise.all( + nameUpdatedSecrets.map(({ newSecretName }) => + generateSecretBlindIndexWithSaltHelper({ + secretName: newSecretName as string, + salt + }) + ) + ).then((blindIndexes) => + blindIndexes.reduce>((prev, curr, i) => { + prev[nameUpdatedSecrets[i].secretName] = curr; + return prev; + }, {}) + ); + const doesAnySecretExistWithNewIndex = await Secret.find({ + workspace: new Types.ObjectId(workspaceId), + folder: folderId, + environment, + secretBlindIndex: { $in: Object.values(newSecretBlindIndexes) } + }); + if (doesAnySecretExistWithNewIndex.length) + throw BadRequestError({ message: "Secret with new name already exist" }); + + const oldSecretsGroupById = oldSecrets.reduce>( + (prev, curr) => ({ ...prev, [curr?.secretBlindIndex || ""]: curr }), + {} + ); + const latestSecretVersions = await getLatestSecretVersion( + updatedSecret.map((el) => oldSecretsGroupById[secretBlindIndexes[el.secretName]]._id) + ); + + commits.push( + ...updatedSecret.map((el) => { + const secretId = oldSecretsGroupById[secretBlindIndexes[el.secretName]]._id; + return { + op: CommitType.UPDATE as const, + secret: secretId, + secretVersion: latestSecretVersions[secretId.toString()]._id, + newVersion: { + ...el, + secretBlindIndex: newSecretBlindIndexes?.[el.secretName], + _id: new Types.ObjectId(), + version: oldSecretsGroupById[secretBlindIndexes[el.secretName]].version || 1 + } + }; + }) + ); + } + + // ----- + // deleted secrets + const deletedSecrets = data[CommitType.DELETE]; + if (deletedSecrets && deletedSecrets.length) { + const secretBlindIndexes = await Promise.all( + deletedSecrets.map(({ secretName }) => + generateSecretBlindIndexWithSaltHelper({ + secretName, + salt + }) + ) + ).then((blindIndexes) => + blindIndexes.reduce>((prev, curr, i) => { + prev[deletedSecrets[i].secretName] = curr; + return prev; + }, {}) + ); + + const secretsToDelete = await Secret.find({ + workspace: new Types.ObjectId(workspaceId), + folder: folderId, + environment, + type: SECRET_SHARED, + secretBlindIndex: { + $in: deletedSecrets.map(({ secretName }) => secretBlindIndexes[secretName]) + } + }) + .select({ secretBlindIndex: 1, _id: 1 }) + .lean() + .exec(); + if (secretsToDelete.length !== deletedSecrets.length) + throw BadRequestError({ message: "Deleted secrets not found" }); + + const oldSecretsGroupById = secretsToDelete.reduce>( + (prev, curr) => ({ ...prev, [curr?.secretBlindIndex || ""]: curr }), + {} + ); + const latestSecretVersions = await getLatestSecretVersion( + deletedSecrets.map((el) => oldSecretsGroupById[secretBlindIndexes[el.secretName]]._id) + ); + + commits.push( + ...deletedSecrets.map((el) => { + const secretId = oldSecretsGroupById[secretBlindIndexes[el.secretName]]._id; + return { + op: CommitType.DELETE as const, + secret: secretId, + secretVersion: latestSecretVersions[secretId.toString()] + }; + }) + ); + } + + const secretApprovalRequest = new SecretApprovalRequest({ + workspace: workspaceId, + environment, + folderId, + policy, + commits, + committer: commiterMembershipId + }); + await secretApprovalRequest.save(); + + await EEAuditLogService.createAuditLog( + authData, + { + type: EventType.SECRET_APPROVAL_REQUEST, + metadata: { + committedBy: commiterMembershipId, + secretApprovalRequestId: secretApprovalRequest._id.toString(), + secretApprovalRequestSlug: secretApprovalRequest.slug + } + }, + { + workspaceId: secretApprovalRequest.workspace + } + ); + + return secretApprovalRequest; +}; + +// validation for a merge conditions happen in another function in controller +export const performSecretApprovalRequestMerge = async ( + id: string, + authData: AuthData, + userMembershipId: string +) => { + const secretApprovalRequest = await SecretApprovalRequest.findById(id) + .populate<{ commits: ISecretCommits }>({ + path: "commits.secret", + select: "+secretBlindIndex", + populate: { + path: "tags" + } + }) + .select("+commits.newVersion.secretBlindIndex"); + if (!secretApprovalRequest) throw BadRequestError({ message: "Approval request not found" }); + + const workspaceId = secretApprovalRequest.workspace; + const environment = secretApprovalRequest.environment; + const folderId = secretApprovalRequest.folderId; + const postHogClient = await TelemetryService.getPostHogClient(); + const conflicts: Array<{ secretId: string; op: CommitType }> = []; + + const secretCreationCommits = secretApprovalRequest.commits.filter( + ({ op }) => op === CommitType.CREATE + ) as Array<{ op: CommitType.CREATE; newVersion: ISecretApprovalSecChange }>; + if (secretCreationCommits.length) { + // the created secrets already exist thus creation conflict ones + const conflictedSecrets = await Secret.find({ + workspace: workspaceId, + environment, + folder: folderId, + secretBlindIndex: { + $in: secretCreationCommits.map(({ newVersion }) => newVersion.secretBlindIndex) + } + }) + .select("+secretBlindIndex") + .lean(); + const conflictGroupByBlindIndex = conflictedSecrets.reduce>( + (prev, curr) => ({ ...prev, [curr.secretBlindIndex || ""]: true }), + {} + ); + const nonConflictSecrets = secretCreationCommits.filter( + ({ newVersion }) => !conflictGroupByBlindIndex[newVersion.secretBlindIndex || ""] + ); + secretCreationCommits + .filter(({ newVersion }) => conflictGroupByBlindIndex[newVersion.secretBlindIndex || ""]) + .forEach((el) => { + conflicts.push({ op: CommitType.CREATE, secretId: el.newVersion._id.toString() }); + }); + + // create secret + const newlyCreatedSecrets: ISecret[] = await Secret.insertMany( + nonConflictSecrets.map( + ({ + newVersion: { + secretKeyIV, + secretKeyTag, + secretValueIV, + secretValueTag, + secretCommentIV, + secretCommentTag, + secretKeyCiphertext, + secretValueCiphertext, + secretCommentCiphertext, + skipMultilineEncoding, + secretBlindIndex, + algorithm, + keyEncoding, + tags + } + }) => ({ + version: 1, + workspace: new Types.ObjectId(workspaceId), + environment, + type: SECRET_SHARED, + secretKeyCiphertext, + secretKeyIV, + secretKeyTag, + secretValueCiphertext, + secretValueIV, + secretValueTag, + secretCommentCiphertext, + secretCommentIV, + secretCommentTag, + folder: folderId, + algorithm: algorithm || ALGORITHM_AES_256_GCM, + keyEncoding: keyEncoding || ENCODING_SCHEME_UTF8, + tags, + skipMultilineEncoding, + secretBlindIndex + }) + ) + ); + + await EESecretService.addSecretVersions({ + secretVersions: newlyCreatedSecrets.map( + (secret) => + new SecretVersion({ + secret: secret._id, + version: secret.version, + workspace: secret.workspace, + type: secret.type, + folder: folderId, + tags: secret.tags, + skipMultilineEncoding: secret?.skipMultilineEncoding, + environment: secret.environment, + isDeleted: false, + secretBlindIndex: secret.secretBlindIndex, + secretKeyCiphertext: secret.secretKeyCiphertext, + secretKeyIV: secret.secretKeyIV, + secretKeyTag: secret.secretKeyTag, + secretValueCiphertext: secret.secretValueCiphertext, + secretValueIV: secret.secretValueIV, + secretValueTag: secret.secretValueTag, + algorithm: ALGORITHM_AES_256_GCM, + keyEncoding: ENCODING_SCHEME_UTF8 + }) + ) + }); + } + + const secretUpdationCommits = secretApprovalRequest.commits.filter( + ({ op }) => op === CommitType.UPDATE + ) as Array<{ + op: CommitType.UPDATE; + newVersion: Partial> & { _id: Types.ObjectId }; + secret: ISecret; + }>; + if (secretUpdationCommits.length) { + const conflictedByNewBlindIndex = await Secret.find({ + workspace: workspaceId, + environment, + folder: folderId, + secretBlindIndex: { + $in: secretUpdationCommits + .map(({ newVersion }) => newVersion?.secretBlindIndex) + .filter(Boolean) + } + }) + .select("+secretBlindIndex") + .lean(); + const conflictGroupByBlindIndex = conflictedByNewBlindIndex.reduce>( + (prev, curr) => (curr?.secretBlindIndex ? { ...prev, [curr.secretBlindIndex]: true } : prev), + {} + ); + secretUpdationCommits + .filter( + ({ newVersion, secret }) => + (newVersion.secretBlindIndex && conflictGroupByBlindIndex[newVersion.secretBlindIndex]) || + !secret + ) + .forEach((el) => { + conflicts.push({ op: CommitType.UPDATE, secretId: el.newVersion._id.toString() }); + }); + + const nonConflictSecrets = secretUpdationCommits.filter( + ({ newVersion, secret }) => + Boolean(secret) && + (newVersion?.secretBlindIndex + ? !conflictGroupByBlindIndex[newVersion.secretBlindIndex] + : true) + ); + await Secret.bulkWrite( + // id and version are stripped off + nonConflictSecrets.map( + ({ + newVersion: { + secretKeyIV, + secretKeyTag, + secretValueIV, + secretValueTag, + secretCommentIV, + secretCommentTag, + secretKeyCiphertext, + secretValueCiphertext, + secretCommentCiphertext, + skipMultilineEncoding, + secretBlindIndex, + tags + }, + secret + }) => ({ + updateOne: { + filter: { + workspace: new Types.ObjectId(workspaceId), + environment, + folder: folderId, + secretBlindIndex: secret.secretBlindIndex, + type: SECRET_SHARED + }, + update: { + $inc: { + version: 1 + }, + secretKeyIV, + secretKeyTag, + secretValueIV, + secretValueTag, + secretCommentIV, + secretCommentTag, + secretKeyCiphertext, + secretValueCiphertext, + secretCommentCiphertext, + skipMultilineEncoding, + secretBlindIndex, + tags, + algorithm: ALGORITHM_AES_256_GCM, + keyEncoding: ENCODING_SCHEME_UTF8 + } + } + }) + ) + ); + + await EESecretService.addSecretVersions({ + secretVersions: nonConflictSecrets.map(({ newVersion, secret }) => { + return new SecretVersion({ + secret: secret._id, + version: secret.version + 1, + workspace: workspaceId, + type: SECRET_SHARED, + folder: folderId, + environment, + isDeleted: false, + secretBlindIndex: newVersion?.secretBlindIndex ?? secret.secretBlindIndex, + secretKeyCiphertext: newVersion?.secretKeyCiphertext ?? secret.secretKeyCiphertext, + secretKeyIV: newVersion?.secretKeyIV ?? secret.secretKeyCiphertext, + secretKeyTag: newVersion?.secretKeyTag ?? secret.secretKeyTag, + secretValueCiphertext: newVersion?.secretValueCiphertext ?? secret.secretValueCiphertext, + secretValueIV: newVersion?.secretValueIV ?? secret.secretValueIV, + secretValueTag: newVersion?.secretValueTag ?? secret.secretValueTag, + tags: newVersion?.tags ?? secret.tags, + algorithm: ALGORITHM_AES_256_GCM, + keyEncoding: ENCODING_SCHEME_UTF8, + skipMultilineEncoding: newVersion?.skipMultilineEncoding ?? secret.skipMultilineEncoding + }); + }) + }); + } + + const secretDeletionCommits = secretApprovalRequest.commits.filter( + ({ op }) => op === CommitType.DELETE + ) as Array<{ + op: CommitType.DELETE; + secret: ISecret; + }>; + if (secretDeletionCommits.length) { + await Secret.deleteMany({ + workspace: new Types.ObjectId(workspaceId), + folder: folderId, + environment + }) + .or( + secretDeletionCommits.map(({ secret: { secretBlindIndex } }) => ({ + secretBlindIndex, + type: { $in: ["shared", "personal"] } + })) + ) + .exec(); + + await EESecretService.markDeletedSecretVersions({ + secretIds: secretDeletionCommits.map(({ secret }) => secret._id) + }); + } + + const updatedSecretApproval = await SecretApprovalRequest.findByIdAndUpdate( + id, + { + conflicts, + hasMerged: true, + status: "close", + statusChangeBy: userMembershipId + }, + { new: true } + ); + + if (postHogClient) { + if (postHogClient) { + postHogClient.capture({ + event: "secrets merged", + distinctId: await TelemetryService.getDistinctId({ + authData + }), + properties: { + numberOfSecrets: secretApprovalRequest.commits.length, + environment, + workspaceId, + folderId, + channel: authData.userAgentType, + userAgent: authData.userAgent + } + }); + } + } + + await EESecretService.takeSecretSnapshot({ + workspaceId, + environment, + folderId + }); + + // question to team where to keep secretKey + await EEAuditLogService.createAuditLog( + authData, + { + type: EventType.SECRET_APPROVAL_MERGED, + metadata: { + mergedBy: userMembershipId, + secretApprovalRequestId: id, + secretApprovalRequestSlug: secretApprovalRequest.slug + } + }, + { + workspaceId + } + ); + + return updatedSecretApproval; +}; diff --git a/backend/src/ee/validation/secretApproval.ts b/backend/src/ee/validation/secretApproval.ts new file mode 100644 index 000000000..999820e48 --- /dev/null +++ b/backend/src/ee/validation/secretApproval.ts @@ -0,0 +1,54 @@ +import { z } from "zod"; + +export const GetSecretApprovalRuleList = z.object({ + query: z.object({ + workspaceId: z.string().trim() + }) +}); + +export const GetSecretApprovalPolicyOfABoard = z.object({ + query: z.object({ + workspaceId: z.string().trim(), + environment: z.string().trim(), + secretPath: z.string().trim() + }) +}); + +export const CreateSecretApprovalRule = z.object({ + body: z + .object({ + workspaceId: z.string(), + name: z.string().optional(), + environment: z.string(), + secretPath: z.string().optional().nullable(), + approvers: z.string().array().min(1), + approvals: z.number().min(1).default(1) + }) + .refine((data) => data.approvals <= data.approvers.length, { + path: ["approvals"], + message: "The number of approvals should be lower than the number of approvers." + }) +}); + +export const UpdateSecretApprovalRule = z.object({ + params: z.object({ + id: z.string() + }), + body: z + .object({ + name: z.string().optional(), + approvers: z.string().array().min(1), + approvals: z.number().min(1).default(1), + secretPath: z.string().optional().nullable() + }) + .refine((data) => data.approvals <= data.approvers.length, { + path: ["approvals"], + message: "The number of approvals should be lower than the number of approvers." + }) +}); + +export const DeleteSecretApprovalRule = z.object({ + params: z.object({ + id: z.string() + }) +}); diff --git a/backend/src/ee/validation/secretApprovalRequest.ts b/backend/src/ee/validation/secretApprovalRequest.ts new file mode 100644 index 000000000..07aff586c --- /dev/null +++ b/backend/src/ee/validation/secretApprovalRequest.ts @@ -0,0 +1,49 @@ +import { z } from "zod"; +import { ApprovalStatus } from "../models/secretApprovalRequest"; + +export const getSecretApprovalRequests = z.object({ + query: z.object({ + workspaceId: z.string().trim(), + environment: z.string().trim().optional(), + committer: z.string().trim().optional(), + status: z.enum(["open", "close"]).optional(), + limit: z.coerce.number().default(20), + offset: z.coerce.number().default(0) + }) +}); + +export const getSecretApprovalRequestCount = z.object({ + query: z.object({ + workspaceId: z.string().trim() + }) +}); + +export const getSecretApprovalRequestDetails = z.object({ + params: z.object({ + id: z.string().trim() + }) +}); + +export const updateSecretApprovalReviewStatus = z.object({ + body: z.object({ + status: z.enum([ApprovalStatus.APPROVED, ApprovalStatus.REJECTED]) + }), + params: z.object({ + id: z.string().trim() + }) +}); + +export const mergeSecretApprovalRequest = z.object({ + params: z.object({ + id: z.string().trim() + }) +}); + +export const updateSecretApprovalRequestStatus = z.object({ + params: z.object({ + id: z.string().trim() + }), + body: z.object({ + status: z.enum(["open", "close"]) + }) +}); diff --git a/backend/src/index.ts b/backend/src/index.ts index 602da13ec..08b159d42 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -25,11 +25,11 @@ import { users as eeUsersRouter, workspace as eeWorkspaceRouter, roles as v1RoleRouter, + secretApprovalPolicy as v1SecretApprovalPolicy, + secretApprovalRequest as v1SecretApprovalRequest, secretScanning as v1SecretScanningRouter } from "./ee/routes/v1"; -import { - serviceTokenData as v3ServiceTokenDataRouter -} from "./ee/routes/v3"; +import { serviceTokenData as v3ServiceTokenDataRouter } from "./ee/routes/v3"; import { auth as v1AuthRouter, bot as v1BotRouter, @@ -42,7 +42,6 @@ import { organization as v1OrganizationRouter, password as v1PasswordRouter, sso as v1SSORouter, - secretApprovalPolicy as v1SecretApprovalPolicy, secretImps as v1SecretImpsRouter, secret as v1SecretRouter, secretsFolder as v1SecretsFolder, @@ -183,6 +182,7 @@ const main = async () => { app.use("/api/v1/roles", v1RoleRouter); app.use("/api/v1/secret-approvals", v1SecretApprovalPolicy); app.use("/api/v1/sso", v1SSORouter); + app.use("/api/v1/secret-approval-requests", v1SecretApprovalRequest); // v2 routes (improvements) app.use("/api/v2/signup", v2SignupRouter); @@ -228,24 +228,23 @@ const main = async () => { // await createTestUserForDevelopment(); setUpHealthEndpoint(server); - const serverCleanup = async () => { await DatabaseService.closeDatabase(); syncSecretsToThirdPartyServices.close(); githubPushEventSecretScan.close(); process.exit(0); - } + }; process.on("SIGINT", function () { server.close(async () => { - await serverCleanup() + await serverCleanup(); }); }); process.on("SIGTERM", function () { server.close(async () => { - await serverCleanup() + await serverCleanup(); }); }); diff --git a/backend/src/models/secretApprovalRequest.ts b/backend/src/models/secretApprovalRequest.ts deleted file mode 100644 index a145b5773..000000000 --- a/backend/src/models/secretApprovalRequest.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { Schema, Types, model } from "mongoose"; -import { ISecretVersion, SecretVersion } from "../ee/models/secretVersion"; - -enum ApprovalStatus { - PENDING = "pending", - APPROVED = "approved", - REJECTED = "rejected" -} - -enum CommitType { - DELETE = "delete", - UPDATE = "update", - CREATE = "create" -} - -export interface ISecretApprovalRequest { - _id: Types.ObjectId; - committer: Types.ObjectId; - approvers: { - member: Types.ObjectId; - status: ApprovalStatus; - }[]; - approvals: number; - hasMerged: boolean; - status: ApprovalStatus; - commits: { - secretVersion: Types.ObjectId; - newVersion: ISecretVersion; - op: CommitType; - }[]; -} - -const secretApprovalRequestSchema = new Schema( - { - approvers: [ - { - member: { - // user associated with the personal secret - type: Schema.Types.ObjectId, - ref: "Membership" - }, - status: { type: String, enum: ApprovalStatus, default: ApprovalStatus.PENDING } - } - ], - approvals: { - type: Number, - required: true - }, - hasMerged: { type: Boolean, default: false }, - status: { type: String, enum: ApprovalStatus, default: ApprovalStatus.PENDING }, - committer: { type: Schema.Types.ObjectId, ref: "Membership" }, - commits: [ - { - secretVersion: { type: Types.ObjectId, ref: "SecretVersion" }, - newVersion: SecretVersion, - op: { type: String, enum: [CommitType], required: true } - } - ] - }, - { - timestamps: true - } -); - -export const SecretApprovalRequest = model( - "SecretApprovalRequest", - secretApprovalRequestSchema -); diff --git a/backend/src/routes/v1/index.ts b/backend/src/routes/v1/index.ts index 50275d00f..2d7bf27ef 100644 --- a/backend/src/routes/v1/index.ts +++ b/backend/src/routes/v1/index.ts @@ -18,7 +18,6 @@ import integrationAuth from "./integrationAuth"; import secretsFolder from "./secretsFolder"; import webhooks from "./webhook"; import secretImps from "./secretImps"; -import secretApprovalPolicy from "./secretApprovalPolicy"; export { signup, @@ -40,6 +39,5 @@ export { secretsFolder, webhooks, secretImps, - sso, - secretApprovalPolicy + sso }; diff --git a/backend/src/validation/index.ts b/backend/src/validation/index.ts index 75552682c..a823f8095 100644 --- a/backend/src/validation/index.ts +++ b/backend/src/validation/index.ts @@ -1,4 +1,3 @@ -export * from "./secretApproval"; export * from "./user"; export * from "./workspace"; export * from "./bot"; diff --git a/backend/src/validation/secretApproval.ts b/backend/src/validation/secretApproval.ts deleted file mode 100644 index 9d4b10ce5..000000000 --- a/backend/src/validation/secretApproval.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { z } from "zod"; - -export const GetSecretApprovalRuleList = z.object({ - query: z.object({ - workspaceId: z.string() - }) -}); - -export const CreateSecretApprovalRule = z.object({ - body: z.object({ - workspaceId: z.string(), - environment: z.string(), - secretPath: z.string().optional().nullable(), - approvers: z.string().array().optional(), - approvals: z.number().min(1).default(1) - }) -}); - -export const UpdateSecretApprovalRule = z.object({ - params: z.object({ - id: z.string() - }), - body: z.object({ - approvers: z.string().array().optional(), - approvals: z.number().min(1).optional(), - secretPath: z.string().optional().nullable() - }) -}); - -export const DeleteSecretApprovalRule = z.object({ - params: z.object({ - id: z.string() - }) -}); diff --git a/frontend/public/lotties/system-outline-189-domain-verification.json b/frontend/public/lotties/system-outline-189-domain-verification.json new file mode 100644 index 000000000..cb38c4784 --- /dev/null +++ b/frontend/public/lotties/system-outline-189-domain-verification.json @@ -0,0 +1 @@ +{"v":"5.8.1","fr":60,"ip":0,"op":91,"w":500,"h":500,"nm":"189-domain-verification-outline","ddd":0,"assets":[{"id":"comp_0","nm":"in-domain","fr":60,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":40,"s":[180]}],"ix":10},"p":{"a":0,"k":[250.004,250.003,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,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.2,"y":1},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[-0.237,-9.809],[9.809,-0.237],[0.237,9.809],[-9.809,0.237]],"o":[[0.237,9.809],[-9.809,0.237],[-0.237,-9.809],[9.809,-0.237]],"v":[[17.524,-176.429],[0.193,-158.24],[-17.996,-175.571],[-0.666,-193.76]],"c":true}]},{"t":40,"s":[{"i":[[-2.572,-106.399],[106.399,-2.572],[2.572,106.399],[-106.399,2.572]],"o":[[2.572,106.399],[-106.399,2.572],[-2.572,-106.399],[106.399,-2.572]],"v":[[192.652,-4.656],[4.656,192.652],[-192.652,4.656],[-4.656,-192.652]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":0,"k":73.3,"ix":1},"e":{"a":0,"k":93.2,"ix":2},"o":{"a":0,"k":-6,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('189-domain-verification-outline').layer('Color & Stroke Change').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":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"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":"Group 2","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.2,"y":1},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[-0.237,-9.809],[9.809,-0.237],[0.237,9.809],[-9.809,0.237]],"o":[[0.237,9.809],[-9.809,0.237],[-0.237,-9.809],[9.809,-0.237]],"v":[[17.524,-176.429],[0.193,-158.24],[-17.996,-175.571],[-0.666,-193.76]],"c":true}]},{"t":40,"s":[{"i":[[-2.572,-106.399],[106.399,-2.572],[2.572,106.399],[-106.399,2.572]],"o":[[2.572,106.399],[-106.399,2.572],[-2.572,-106.399],[106.399,-2.572]],"v":[[192.652,-4.656],[4.656,192.652],[-192.652,4.656],[-4.656,-192.652]],"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":66.5,"ix":2},"o":{"a":0,"k":-6,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('189-domain-verification-outline').layer('Color & Stroke Change').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":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"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":"Group 1","np":3,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"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":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"t":15,"s":[180]},{"t":40,"s":[360]}],"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":1,"op":41,"st":0,"bm":0},{"ddd":0,"ind":2,"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.004,250.003,0],"ix":2,"l":2},"a":{"a":0,"k":[250.004,250.003,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,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.51,0],[0.51,0.05],[0.5,0.1],[0.479,0.15],[0.47,0.2],[0.45,0.24],[0.42,0.28],[0.39,0.32],[0.36,0.36],[0.319,0.4],[0.279,0.42],[0.239,0.45],[0.19,0.47],[0.149,0.49],[0.1,0.5],[0.05,0.5],[0,0.51],[-0.06,0.51],[-0.101,0.5],[-0.15,0.49],[-0.2,0.47],[-0.24,0.45],[-0.28,0.42],[-0.32,0.39],[-0.359,0.36],[-0.4,0.32],[-0.42,0.28],[-0.45,0.24],[-0.47,0.19],[-0.48,0.15],[-0.5,0.1],[-0.51,0.05],[-1.021,-0.11],[-0.5,-0.1],[-0.48,-0.15],[-0.46,-0.2],[-0.45,-0.24],[-0.43,-0.29],[-0.39,-0.32],[-0.359,-0.36],[-0.33,-0.4],[-0.28,-0.43],[-0.24,-0.45],[-0.2,-0.47],[-0.149,-0.48],[-0.1,-0.5],[-0.051,-0.51],[0,-0.51],[0.05,-0.51],[0.1,-0.5],[0.141,-0.48],[0.189,-0.47],[0.23,-0.45],[0.28,-0.43],[0.32,-0.39],[0.37,-0.37],[0.4,-0.33],[0.421,-0.28],[0.45,-0.24],[0.48,-0.19],[0.49,-0.15],[0.5,-0.1],[0.51,-0.05]],"o":[[-0.51,0],[-0.51,-0.05],[-0.5,-0.1],[-0.49,-0.15],[-0.47,-0.19],[-0.45,-0.24],[-0.42,-0.28],[-0.4,-0.33],[-0.359,-0.37],[-0.32,-0.39],[-0.28,-0.43],[-0.24,-0.45],[-0.2,-0.47],[-0.15,-0.48],[-0.101,-0.5],[-0.06,-0.51],[0,-0.51],[0.05,-0.51],[0.1,-0.5],[0.149,-0.48],[0.19,-0.47],[0.239,-0.45],[0.279,-0.43],[0.319,-0.4],[0.36,-0.36],[0.39,-0.32],[0.42,-0.29],[0.45,-0.24],[0.47,-0.2],[0.489,-0.15],[0.5,-0.1],[1.02,-0.11],[0.51,0.05],[0.5,0.1],[0.49,0.15],[0.48,0.19],[0.45,0.24],[0.421,0.28],[0.4,0.32],[0.37,0.36],[0.32,0.39],[0.28,0.42],[0.23,0.45],[0.189,0.47],[0.141,0.49],[0.1,0.5],[0.05,0.51],[0,0.51],[-0.051,0.5],[-0.1,0.5],[-0.149,0.49],[-0.2,0.48],[-0.24,0.45],[-0.28,0.42],[-0.33,0.4],[-0.359,0.36],[-0.39,0.32],[-0.43,0.28],[-0.45,0.24],[-0.46,0.2],[-0.48,0.15],[-0.5,0.1],[-0.51,0.05]],"v":[[0,15.651],[-1.54,15.571],[-3.061,15.341],[-4.54,14.971],[-5.99,14.451],[-7.37,13.801],[-8.69,13.011],[-9.92,12.101],[-11.07,11.071],[-12.1,9.921],[-13.01,8.691],[-13.8,7.371],[-14.45,5.981],[-14.97,4.541],[-15.35,3.061],[-15.57,1.541],[-15.65,0.001],[-15.57,-1.539],[-15.35,-3.059],[-14.97,-4.549],[-14.45,-5.989],[-13.8,-7.368],[-13.01,-8.689],[-12.1,-9.919],[-11.07,-11.069],[-9.92,-12.099],[-8.69,-13.009],[-7.37,-13.799],[-5.99,-14.449],[-4.55,-14.969],[-3.061,-15.349],[-1.54,-15.569],[1.54,-15.569],[3.06,-15.349],[4.54,-14.969],[5.979,-14.449],[7.37,-13.799],[8.689,-13.009],[9.92,-12.099],[11.06,-11.069],[12.1,-9.919],[13.01,-8.689],[13.8,-7.368],[14.45,-5.989],[14.97,-4.549],[15.34,-3.059],[15.57,-1.539],[15.65,0.001],[15.57,1.541],[15.34,3.061],[14.97,4.541],[14.45,5.981],[13.8,7.371],[13.01,8.691],[12.1,9.921],[11.06,11.071],[9.92,12.101],[8.689,13.011],[7.37,13.801],[5.979,14.451],[4.54,14.971],[3.06,15.341],[1.54,15.571]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('189-domain-verification-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[296.88,125.002],"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.51,0],[0.51,0.05],[0.5,0.1],[0.479,0.15],[0.47,0.19],[0.45,0.24],[0.431,0.28],[0.39,0.32],[0.36,0.36],[0.319,0.4],[0.279,0.42],[0.239,0.45],[0.19,0.47],[0.149,0.49],[0.109,0.49],[0.05,0.5],[0,0.51],[-0.06,0.51],[-0.101,0.5],[-0.15,0.48],[-0.2,0.47],[-0.24,0.45],[-0.28,0.43],[-0.32,0.39],[-0.359,0.37],[-0.4,0.32],[-0.42,0.28],[-0.45,0.24],[-0.47,0.19],[-0.48,0.15],[-0.5,0.11],[-0.51,0.05],[-1.021,-0.11],[-0.5,-0.1],[-0.48,-0.15],[-0.47,-0.2],[-0.45,-0.24],[-0.43,-0.28],[-0.39,-0.32],[-0.359,-0.36],[-0.33,-0.4],[-0.28,-0.42],[-0.24,-0.45],[-0.19,-0.47],[-0.149,-0.49],[-0.1,-0.5],[-0.051,-0.51],[0,-0.51],[0.05,-0.51],[0.1,-0.5],[0.15,-0.49],[0.189,-0.47],[0.24,-0.45],[0.28,-0.43],[0.32,-0.39],[0.37,-0.37],[0.4,-0.33],[0.421,-0.28],[0.45,-0.24],[0.471,-0.19],[0.49,-0.15],[0.5,-0.1],[0.5,-0.05]],"o":[[-0.51,0],[-0.51,-0.05],[-0.5,-0.1],[-0.49,-0.15],[-0.47,-0.19],[-0.45,-0.24],[-0.42,-0.28],[-0.4,-0.33],[-0.359,-0.37],[-0.32,-0.39],[-0.28,-0.43],[-0.24,-0.45],[-0.2,-0.47],[-0.15,-0.49],[-0.101,-0.5],[-0.06,-0.51],[0,-0.51],[0.05,-0.51],[0.109,-0.5],[0.149,-0.49],[0.19,-0.47],[0.239,-0.45],[0.279,-0.42],[0.319,-0.4],[0.36,-0.36],[0.39,-0.32],[0.42,-0.28],[0.45,-0.24],[0.47,-0.2],[0.489,-0.15],[0.5,-0.1],[1.02,-0.11],[0.5,0.05],[0.5,0.11],[0.49,0.15],[0.471,0.19],[0.45,0.24],[0.421,0.28],[0.4,0.32],[0.37,0.37],[0.32,0.39],[0.28,0.43],[0.24,0.45],[0.189,0.47],[0.15,0.48],[0.1,0.5],[0.05,0.51],[0,0.51],[-0.051,0.5],[-0.1,0.49],[-0.149,0.49],[-0.19,0.47],[-0.24,0.45],[-0.28,0.42],[-0.33,0.4],[-0.359,0.36],[-0.39,0.32],[-0.43,0.28],[-0.45,0.24],[-0.47,0.19],[-0.48,0.15],[-0.5,0.1],[-0.51,0.05]],"v":[[0,15.651],[-1.54,15.571],[-3.061,15.341],[-4.54,14.971],[-5.99,14.451],[-7.37,13.801],[-8.69,13.011],[-9.92,12.101],[-11.07,11.071],[-12.1,9.921],[-13.01,8.691],[-13.8,7.371],[-14.45,5.981],[-14.97,4.541],[-15.35,3.061],[-15.57,1.541],[-15.65,0.001],[-15.57,-1.539],[-15.35,-3.059],[-14.97,-4.539],[-14.45,-5.989],[-13.8,-7.368],[-13.01,-8.689],[-12.1,-9.919],[-11.07,-11.069],[-9.92,-12.099],[-8.69,-13.009],[-7.37,-13.799],[-5.99,-14.449],[-4.55,-14.969],[-3.061,-15.349],[-1.54,-15.569],[1.54,-15.569],[3.06,-15.349],[4.54,-14.969],[5.979,-14.449],[7.37,-13.799],[8.689,-13.009],[9.92,-12.099],[11.06,-11.069],[12.1,-9.919],[13.01,-8.689],[13.8,-7.368],[14.45,-5.989],[14.97,-4.539],[15.34,-3.059],[15.57,-1.539],[15.65,0.001],[15.57,1.541],[15.34,3.061],[14.97,4.541],[14.45,5.981],[13.8,7.371],[13.01,8.691],[12.1,9.921],[11.06,11.071],[9.92,12.101],[8.689,13.011],[7.37,13.801],[5.979,14.451],[4.54,14.971],[3.06,15.341],[1.54,15.571]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('189-domain-verification-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[359.38,125.002],"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":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[8.644,0],[0,0],[0,8.643],[-8.644,0],[0,0],[0,-8.643]],"o":[[0,0],[-8.644,0],[0,-8.643],[0,0],[8.644,0],[0,8.643]],"v":[[171.876,15.65],[-171.876,15.65],[-187.526,0],[-171.876,-15.65],[171.876,-15.65],[187.526,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('189-domain-verification-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[250.004,171.878],"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 3","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[250.004,171.878],"ix":2},"a":{"a":0,"k":[250.004,171.878],"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":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[4.002,0],[3.056,3.051],[0,0],[-6.107,6.116],[-6.116,-6.108],[0,0],[0,0],[-6.107,-6.115],[6.115,-6.108],[0,0]],"o":[[-4.002,0],[0,0],[-6.115,-6.108],[6.108,-6.114],[0,0],[0,0],[6.115,-6.107],[6.108,6.115],[0,0],[-3.055,3.051]],"v":[[-23.984,61.442],[-35.043,56.866],[-78.768,13.195],[-78.782,-8.938],[-56.649,-8.951],[-23.984,23.673],[56.649,-56.865],[78.781,-56.852],[78.768,-34.72],[-12.924,56.865]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('189-domain-verification-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[250.003,292.753],"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,-2.858],[0,0],[-2.858,0],[0,0],[0,2.858],[0,0],[2.858,0]],"o":[[-2.858,0],[0,0],[0,2.858],[0,0],[2.858,0],[0,0],[0,-2.858],[0,0]],"v":[[-151.042,-156.226],[-156.226,-151.043],[-156.226,151.042],[-151.042,156.226],[151.042,156.226],[156.226,151.042],[156.226,-151.043],[151.042,-156.226]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[20.117,0],[0,0],[0,20.117],[0,0],[-20.117,0],[0,0],[0,-20.117],[0,0]],"o":[[0,0],[-20.117,0],[0,0],[0,-20.117],[0,0],[20.117,0],[0,0],[0,20.117]],"v":[[151.042,187.526],[-151.042,187.526],[-187.526,151.042],[-187.526,-151.043],[-151.042,-187.526],[151.042,-187.526],[187.526,-151.043],[187.526,151.042]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('189-domain-verification-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[250.004,250.003],"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},{"ty":"tr","p":{"a":0,"k":[250.004,250.003],"ix":2},"a":{"a":0,"k":[250.004,250.003],"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}],"ip":90,"op":300,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".primary.design","cl":"primary design","parent":6,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,42.749,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,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[-81.604,66.643],[-34.5,-33],[34.5,-35.5]],"o":[[0,0],[0,0],[0,0],[90,-73.5],[46.014,44.014],[0,0]],"v":[[67.709,-45.792],[-23.984,45.792],[-67.709,2.122],[-118.504,-193.753],[116.997,-195.253],[67.997,-45.753]],"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.333],"y":[0]},"t":0,"s":[22.5]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":15,"s":[28.5]},{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":60,"s":[28.5]},{"t":90,"s":[22.5]}],"ix":2},"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":15,"s":[181]},{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":60,"s":[181]},{"t":90,"s":[360]}],"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('189-domain-verification-outline').layer('Color & Stroke Change').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":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"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":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":40,"op":90,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":".primary.design","cl":"primary design","td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250.004,250.003,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,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":1,"s":[{"i":[[11.505,0],[0,0],[0,-11.506],[0,0],[-11.506,0],[0,0],[0,11.505],[0,0]],"o":[[0,0],[-11.506,0],[0,0],[0,11.505],[0,0],[11.505,0],[0,0],[0,-11.506]],"v":[[151.042,-171.876],[-151.042,-171.876],[-171.876,-151.042],[-171.876,151.042],[-151.042,171.876],[151.042,171.876],[171.876,151.042],[171.876,-151.042]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.333,"y":0},"t":15,"s":[{"i":[[107.136,0],[0,0],[0,-106.591],[0,0],[-106.043,0],[0,0],[0,106.032],[0,0]],"o":[[0,0],[-106.591,0],[0,0],[0,106.031],[0,0],[106.032,0],[0,0],[0,-107.148]],"v":[[-1.004,-192.997],[-0.004,-192.997],[-192.997,-0.004],[-192.997,0.997],[-0.997,192.997],[0.996,192.997],[192.997,0.996],[192.997,1.004]],"c":true}]},{"i":{"x":0.2,"y":1},"o":{"x":0.167,"y":0},"t":60,"s":[{"i":[[107.136,0],[0,0],[0,-106.591],[0,0],[-106.043,0],[0,0],[0,106.032],[0,0]],"o":[[0,0],[-106.591,0],[0,0],[0,106.031],[0,0],[106.032,0],[0,0],[0,-107.148]],"v":[[-1.004,-192.997],[-0.004,-192.997],[-192.997,-0.004],[-192.997,0.997],[-0.997,192.997],[0.996,192.997],[192.997,0.996],[192.997,1.004]],"c":true}]},{"t":90,"s":[{"i":[[11.505,0],[0,0],[0,-11.506],[0,0],[-11.506,0],[0,0],[0,11.505],[0,0]],"o":[[0,0],[-11.506,0],[0,0],[0,11.505],[0,0],[11.505,0],[0,0],[0,-11.506]],"v":[[151.042,-171.876],[-151.042,-171.876],[-171.876,-151.042],[-171.876,151.042],[-151.042,171.876],[151.042,171.876],[171.876,151.042],[171.876,-151.042]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"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":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":40,"op":90,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":".primary.design","cl":"primary design","tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":1,"s":[250.004,148.439,0],"to":[0,-32.333,0],"ti":[0,32.333,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.333,"y":0.333},"t":15,"s":[250.004,-45.561,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.2,"y":1},"o":{"x":0.333,"y":0},"t":60,"s":[250.004,-45.561,0],"to":[0,32.333,0],"ti":[0,-32.333,0]},{"t":90,"s":[250.004,148.439,0]}],"ix":2,"l":2},"a":{"a":0,"k":[250.004,148.439,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,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.51,0],[0.51,0.05],[0.5,0.1],[0.479,0.15],[0.47,0.2],[0.45,0.24],[0.42,0.28],[0.39,0.32],[0.36,0.36],[0.319,0.4],[0.279,0.42],[0.239,0.45],[0.19,0.47],[0.149,0.49],[0.1,0.5],[0.05,0.5],[0,0.51],[-0.06,0.51],[-0.101,0.5],[-0.15,0.49],[-0.2,0.47],[-0.24,0.45],[-0.28,0.42],[-0.32,0.39],[-0.359,0.36],[-0.4,0.32],[-0.42,0.28],[-0.45,0.24],[-0.47,0.19],[-0.48,0.15],[-0.5,0.1],[-0.51,0.05],[-1.021,-0.11],[-0.5,-0.1],[-0.48,-0.15],[-0.46,-0.2],[-0.45,-0.24],[-0.43,-0.29],[-0.39,-0.32],[-0.359,-0.36],[-0.33,-0.4],[-0.28,-0.43],[-0.24,-0.45],[-0.2,-0.47],[-0.149,-0.48],[-0.1,-0.5],[-0.051,-0.51],[0,-0.51],[0.05,-0.51],[0.1,-0.5],[0.141,-0.48],[0.189,-0.47],[0.23,-0.45],[0.28,-0.43],[0.32,-0.39],[0.37,-0.37],[0.4,-0.33],[0.421,-0.28],[0.45,-0.24],[0.48,-0.19],[0.49,-0.15],[0.5,-0.1],[0.51,-0.05]],"o":[[-0.51,0],[-0.51,-0.05],[-0.5,-0.1],[-0.49,-0.15],[-0.47,-0.19],[-0.45,-0.24],[-0.42,-0.28],[-0.4,-0.33],[-0.359,-0.37],[-0.32,-0.39],[-0.28,-0.43],[-0.24,-0.45],[-0.2,-0.47],[-0.15,-0.48],[-0.101,-0.5],[-0.06,-0.51],[0,-0.51],[0.05,-0.51],[0.1,-0.5],[0.149,-0.48],[0.19,-0.47],[0.239,-0.45],[0.279,-0.43],[0.319,-0.4],[0.36,-0.36],[0.39,-0.32],[0.42,-0.29],[0.45,-0.24],[0.47,-0.2],[0.489,-0.15],[0.5,-0.1],[1.02,-0.11],[0.51,0.05],[0.5,0.1],[0.49,0.15],[0.48,0.19],[0.45,0.24],[0.421,0.28],[0.4,0.32],[0.37,0.36],[0.32,0.39],[0.28,0.42],[0.23,0.45],[0.189,0.47],[0.141,0.49],[0.1,0.5],[0.05,0.51],[0,0.51],[-0.051,0.5],[-0.1,0.5],[-0.149,0.49],[-0.2,0.48],[-0.24,0.45],[-0.28,0.42],[-0.33,0.4],[-0.359,0.36],[-0.39,0.32],[-0.43,0.28],[-0.45,0.24],[-0.46,0.2],[-0.48,0.15],[-0.5,0.1],[-0.51,0.05]],"v":[[0,15.651],[-1.54,15.571],[-3.061,15.341],[-4.54,14.971],[-5.99,14.451],[-7.37,13.801],[-8.69,13.011],[-9.92,12.101],[-11.07,11.071],[-12.1,9.921],[-13.01,8.691],[-13.8,7.371],[-14.45,5.981],[-14.97,4.541],[-15.35,3.061],[-15.57,1.541],[-15.65,0.001],[-15.57,-1.539],[-15.35,-3.059],[-14.97,-4.549],[-14.45,-5.989],[-13.8,-7.368],[-13.01,-8.689],[-12.1,-9.919],[-11.07,-11.069],[-9.92,-12.099],[-8.69,-13.009],[-7.37,-13.799],[-5.99,-14.449],[-4.55,-14.969],[-3.061,-15.349],[-1.54,-15.569],[1.54,-15.569],[3.06,-15.349],[4.54,-14.969],[5.979,-14.449],[7.37,-13.799],[8.689,-13.009],[9.92,-12.099],[11.06,-11.069],[12.1,-9.919],[13.01,-8.689],[13.8,-7.368],[14.45,-5.989],[14.97,-4.549],[15.34,-3.059],[15.57,-1.539],[15.65,0.001],[15.57,1.541],[15.34,3.061],[14.97,4.541],[14.45,5.981],[13.8,7.371],[13.01,8.691],[12.1,9.921],[11.06,11.071],[9.92,12.101],[8.689,13.011],[7.37,13.801],[5.979,14.451],[4.54,14.971],[3.06,15.341],[1.54,15.571]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('189-domain-verification-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[296.88,125.002],"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.51,0],[0.51,0.05],[0.5,0.1],[0.479,0.15],[0.47,0.19],[0.45,0.24],[0.431,0.28],[0.39,0.32],[0.36,0.36],[0.319,0.4],[0.279,0.42],[0.239,0.45],[0.19,0.47],[0.149,0.49],[0.109,0.49],[0.05,0.5],[0,0.51],[-0.06,0.51],[-0.101,0.5],[-0.15,0.48],[-0.2,0.47],[-0.24,0.45],[-0.28,0.43],[-0.32,0.39],[-0.359,0.37],[-0.4,0.32],[-0.42,0.28],[-0.45,0.24],[-0.47,0.19],[-0.48,0.15],[-0.5,0.11],[-0.51,0.05],[-1.021,-0.11],[-0.5,-0.1],[-0.48,-0.15],[-0.47,-0.2],[-0.45,-0.24],[-0.43,-0.28],[-0.39,-0.32],[-0.359,-0.36],[-0.33,-0.4],[-0.28,-0.42],[-0.24,-0.45],[-0.19,-0.47],[-0.149,-0.49],[-0.1,-0.5],[-0.051,-0.51],[0,-0.51],[0.05,-0.51],[0.1,-0.5],[0.15,-0.49],[0.189,-0.47],[0.24,-0.45],[0.28,-0.43],[0.32,-0.39],[0.37,-0.37],[0.4,-0.33],[0.421,-0.28],[0.45,-0.24],[0.471,-0.19],[0.49,-0.15],[0.5,-0.1],[0.5,-0.05]],"o":[[-0.51,0],[-0.51,-0.05],[-0.5,-0.1],[-0.49,-0.15],[-0.47,-0.19],[-0.45,-0.24],[-0.42,-0.28],[-0.4,-0.33],[-0.359,-0.37],[-0.32,-0.39],[-0.28,-0.43],[-0.24,-0.45],[-0.2,-0.47],[-0.15,-0.49],[-0.101,-0.5],[-0.06,-0.51],[0,-0.51],[0.05,-0.51],[0.109,-0.5],[0.149,-0.49],[0.19,-0.47],[0.239,-0.45],[0.279,-0.42],[0.319,-0.4],[0.36,-0.36],[0.39,-0.32],[0.42,-0.28],[0.45,-0.24],[0.47,-0.2],[0.489,-0.15],[0.5,-0.1],[1.02,-0.11],[0.5,0.05],[0.5,0.11],[0.49,0.15],[0.471,0.19],[0.45,0.24],[0.421,0.28],[0.4,0.32],[0.37,0.37],[0.32,0.39],[0.28,0.43],[0.24,0.45],[0.189,0.47],[0.15,0.48],[0.1,0.5],[0.05,0.51],[0,0.51],[-0.051,0.5],[-0.1,0.49],[-0.149,0.49],[-0.19,0.47],[-0.24,0.45],[-0.28,0.42],[-0.33,0.4],[-0.359,0.36],[-0.39,0.32],[-0.43,0.28],[-0.45,0.24],[-0.47,0.19],[-0.48,0.15],[-0.5,0.1],[-0.51,0.05]],"v":[[0,15.651],[-1.54,15.571],[-3.061,15.341],[-4.54,14.971],[-5.99,14.451],[-7.37,13.801],[-8.69,13.011],[-9.92,12.101],[-11.07,11.071],[-12.1,9.921],[-13.01,8.691],[-13.8,7.371],[-14.45,5.981],[-14.97,4.541],[-15.35,3.061],[-15.57,1.541],[-15.65,0.001],[-15.57,-1.539],[-15.35,-3.059],[-14.97,-4.539],[-14.45,-5.989],[-13.8,-7.368],[-13.01,-8.689],[-12.1,-9.919],[-11.07,-11.069],[-9.92,-12.099],[-8.69,-13.009],[-7.37,-13.799],[-5.99,-14.449],[-4.55,-14.969],[-3.061,-15.349],[-1.54,-15.569],[1.54,-15.569],[3.06,-15.349],[4.54,-14.969],[5.979,-14.449],[7.37,-13.799],[8.689,-13.009],[9.92,-12.099],[11.06,-11.069],[12.1,-9.919],[13.01,-8.689],[13.8,-7.368],[14.45,-5.989],[14.97,-4.539],[15.34,-3.059],[15.57,-1.539],[15.65,0.001],[15.57,1.541],[15.34,3.061],[14.97,4.541],[14.45,5.981],[13.8,7.371],[13.01,8.691],[12.1,9.921],[11.06,11.071],[9.92,12.101],[8.689,13.011],[7.37,13.801],[5.979,14.451],[4.54,14.971],[3.06,15.341],[1.54,15.571]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('189-domain-verification-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[359.38,125.002],"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":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[8.644,0],[0,0],[0,8.643],[-8.644,0],[0,0],[0,-8.643]],"o":[[0,0],[-8.644,0],[0,-8.643],[0,0],[8.644,0],[0,8.643]],"v":[[171.876,15.65],[-171.876,15.65],[-187.526,0],[-171.876,-15.65],[171.876,-15.65],[187.526,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('189-domain-verification-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[250.004,171.878],"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 3","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[250.004,171.878],"ix":2},"a":{"a":0,"k":[250.004,171.878],"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":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":40,"op":90,"st":1,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"t":1,"s":[0]},{"t":60,"s":[360]}],"ix":10},"p":{"a":0,"k":[250.004,250.003,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,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":1,"s":[{"i":[[11.505,0],[0,0],[0,-11.506],[0,0],[-11.506,0],[0,0],[0,11.505],[0,0]],"o":[[0,0],[-11.506,0],[0,0],[0,11.505],[0,0],[11.505,0],[0,0],[0,-11.506]],"v":[[151.042,-171.876],[-151.042,-171.876],[-171.876,-151.042],[-171.876,151.042],[-151.042,171.876],[151.042,171.876],[171.876,151.042],[171.876,-151.042]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":15,"s":[{"i":[[107.136,0],[0,0],[0,-106.591],[0,0],[-106.043,0],[0,0],[0,106.032],[0,0]],"o":[[0,0],[-106.591,0],[0,0],[0,106.031],[0,0],[106.032,0],[0,0],[0,-107.148]],"v":[[-1.004,-192.997],[-0.004,-192.997],[-192.997,-0.004],[-192.997,0.997],[-0.997,192.997],[0.996,192.997],[192.997,0.996],[192.997,1.004]],"c":true}]},{"i":{"x":0.2,"y":1},"o":{"x":0.167,"y":0.167},"t":60,"s":[{"i":[[107.136,0],[0,0],[0,-106.591],[0,0],[-106.043,0],[0,0],[0,106.032],[0,0]],"o":[[0,0],[-106.591,0],[0,0],[0,106.031],[0,0],[106.032,0],[0,0],[0,-107.148]],"v":[[-1.004,-192.997],[-0.004,-192.997],[-192.997,-0.004],[-192.997,0.997],[-0.997,192.997],[0.996,192.997],[192.997,0.996],[192.997,1.004]],"c":true}]},{"t":90,"s":[{"i":[[11.505,0],[0,0],[0,-11.506],[0,0],[-11.506,0],[0,0],[0,11.505],[0,0]],"o":[[0,0],[-11.506,0],[0,0],[0,11.505],[0,0],[11.505,0],[0,0],[0,-11.506]],"v":[[151.042,-171.876],[-151.042,-171.876],[-171.876,-151.042],[-171.876,151.042],[-151.042,171.876],[151.042,171.876],[171.876,151.042],[171.876,-151.042]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":7,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":9,"s":[7]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":14,"s":[0]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.299],"y":[0]},"t":60,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":70,"s":[0]},{"t":72,"s":[0]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":7,"s":[100]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":14,"s":[66.5]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0]},"t":60,"s":[66.5]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":70,"s":[89.5]},{"t":72,"s":[100]}],"ix":2},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"t":60,"s":[60]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":70,"s":[16]},{"t":72,"s":[0]}],"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('189-domain-verification-outline').layer('Color & Stroke Change').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":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"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":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":40,"op":90,"st":0,"bm":0}]},{"id":"comp_1","nm":"hover-domain","fr":60,"layers":[{"ddd":0,"ind":1,"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.004,250.003,0],"ix":2,"l":2},"a":{"a":0,"k":[250.004,250.003,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,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.51,0],[0.51,0.05],[0.5,0.1],[0.479,0.15],[0.47,0.2],[0.45,0.24],[0.42,0.28],[0.39,0.32],[0.36,0.36],[0.319,0.4],[0.279,0.42],[0.239,0.45],[0.19,0.47],[0.149,0.49],[0.1,0.5],[0.05,0.5],[0,0.51],[-0.06,0.51],[-0.101,0.5],[-0.15,0.49],[-0.2,0.47],[-0.24,0.45],[-0.28,0.42],[-0.32,0.39],[-0.359,0.36],[-0.4,0.32],[-0.42,0.28],[-0.45,0.24],[-0.47,0.19],[-0.48,0.15],[-0.5,0.1],[-0.51,0.05],[-1.021,-0.11],[-0.5,-0.1],[-0.48,-0.15],[-0.46,-0.2],[-0.45,-0.24],[-0.43,-0.29],[-0.39,-0.32],[-0.359,-0.36],[-0.33,-0.4],[-0.28,-0.43],[-0.24,-0.45],[-0.2,-0.47],[-0.149,-0.48],[-0.1,-0.5],[-0.051,-0.51],[0,-0.51],[0.05,-0.51],[0.1,-0.5],[0.141,-0.48],[0.189,-0.47],[0.23,-0.45],[0.28,-0.43],[0.32,-0.39],[0.37,-0.37],[0.4,-0.33],[0.421,-0.28],[0.45,-0.24],[0.48,-0.19],[0.49,-0.15],[0.5,-0.1],[0.51,-0.05]],"o":[[-0.51,0],[-0.51,-0.05],[-0.5,-0.1],[-0.49,-0.15],[-0.47,-0.19],[-0.45,-0.24],[-0.42,-0.28],[-0.4,-0.33],[-0.359,-0.37],[-0.32,-0.39],[-0.28,-0.43],[-0.24,-0.45],[-0.2,-0.47],[-0.15,-0.48],[-0.101,-0.5],[-0.06,-0.51],[0,-0.51],[0.05,-0.51],[0.1,-0.5],[0.149,-0.48],[0.19,-0.47],[0.239,-0.45],[0.279,-0.43],[0.319,-0.4],[0.36,-0.36],[0.39,-0.32],[0.42,-0.29],[0.45,-0.24],[0.47,-0.2],[0.489,-0.15],[0.5,-0.1],[1.02,-0.11],[0.51,0.05],[0.5,0.1],[0.49,0.15],[0.48,0.19],[0.45,0.24],[0.421,0.28],[0.4,0.32],[0.37,0.36],[0.32,0.39],[0.28,0.42],[0.23,0.45],[0.189,0.47],[0.141,0.49],[0.1,0.5],[0.05,0.51],[0,0.51],[-0.051,0.5],[-0.1,0.5],[-0.149,0.49],[-0.2,0.48],[-0.24,0.45],[-0.28,0.42],[-0.33,0.4],[-0.359,0.36],[-0.39,0.32],[-0.43,0.28],[-0.45,0.24],[-0.46,0.2],[-0.48,0.15],[-0.5,0.1],[-0.51,0.05]],"v":[[0,15.651],[-1.54,15.571],[-3.061,15.341],[-4.54,14.971],[-5.99,14.451],[-7.37,13.801],[-8.69,13.011],[-9.92,12.101],[-11.07,11.071],[-12.1,9.921],[-13.01,8.691],[-13.8,7.371],[-14.45,5.981],[-14.97,4.541],[-15.35,3.061],[-15.57,1.541],[-15.65,0.001],[-15.57,-1.539],[-15.35,-3.059],[-14.97,-4.549],[-14.45,-5.989],[-13.8,-7.368],[-13.01,-8.689],[-12.1,-9.919],[-11.07,-11.069],[-9.92,-12.099],[-8.69,-13.009],[-7.37,-13.799],[-5.99,-14.449],[-4.55,-14.969],[-3.061,-15.349],[-1.54,-15.569],[1.54,-15.569],[3.06,-15.349],[4.54,-14.969],[5.979,-14.449],[7.37,-13.799],[8.689,-13.009],[9.92,-12.099],[11.06,-11.069],[12.1,-9.919],[13.01,-8.689],[13.8,-7.368],[14.45,-5.989],[14.97,-4.549],[15.34,-3.059],[15.57,-1.539],[15.65,0.001],[15.57,1.541],[15.34,3.061],[14.97,4.541],[14.45,5.981],[13.8,7.371],[13.01,8.691],[12.1,9.921],[11.06,11.071],[9.92,12.101],[8.689,13.011],[7.37,13.801],[5.979,14.451],[4.54,14.971],[3.06,15.341],[1.54,15.571]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('189-domain-verification-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[296.88,125.002],"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.51,0],[0.51,0.05],[0.5,0.1],[0.479,0.15],[0.47,0.19],[0.45,0.24],[0.431,0.28],[0.39,0.32],[0.36,0.36],[0.319,0.4],[0.279,0.42],[0.239,0.45],[0.19,0.47],[0.149,0.49],[0.109,0.49],[0.05,0.5],[0,0.51],[-0.06,0.51],[-0.101,0.5],[-0.15,0.48],[-0.2,0.47],[-0.24,0.45],[-0.28,0.43],[-0.32,0.39],[-0.359,0.37],[-0.4,0.32],[-0.42,0.28],[-0.45,0.24],[-0.47,0.19],[-0.48,0.15],[-0.5,0.11],[-0.51,0.05],[-1.021,-0.11],[-0.5,-0.1],[-0.48,-0.15],[-0.47,-0.2],[-0.45,-0.24],[-0.43,-0.28],[-0.39,-0.32],[-0.359,-0.36],[-0.33,-0.4],[-0.28,-0.42],[-0.24,-0.45],[-0.19,-0.47],[-0.149,-0.49],[-0.1,-0.5],[-0.051,-0.51],[0,-0.51],[0.05,-0.51],[0.1,-0.5],[0.15,-0.49],[0.189,-0.47],[0.24,-0.45],[0.28,-0.43],[0.32,-0.39],[0.37,-0.37],[0.4,-0.33],[0.421,-0.28],[0.45,-0.24],[0.471,-0.19],[0.49,-0.15],[0.5,-0.1],[0.5,-0.05]],"o":[[-0.51,0],[-0.51,-0.05],[-0.5,-0.1],[-0.49,-0.15],[-0.47,-0.19],[-0.45,-0.24],[-0.42,-0.28],[-0.4,-0.33],[-0.359,-0.37],[-0.32,-0.39],[-0.28,-0.43],[-0.24,-0.45],[-0.2,-0.47],[-0.15,-0.49],[-0.101,-0.5],[-0.06,-0.51],[0,-0.51],[0.05,-0.51],[0.109,-0.5],[0.149,-0.49],[0.19,-0.47],[0.239,-0.45],[0.279,-0.42],[0.319,-0.4],[0.36,-0.36],[0.39,-0.32],[0.42,-0.28],[0.45,-0.24],[0.47,-0.2],[0.489,-0.15],[0.5,-0.1],[1.02,-0.11],[0.5,0.05],[0.5,0.11],[0.49,0.15],[0.471,0.19],[0.45,0.24],[0.421,0.28],[0.4,0.32],[0.37,0.37],[0.32,0.39],[0.28,0.43],[0.24,0.45],[0.189,0.47],[0.15,0.48],[0.1,0.5],[0.05,0.51],[0,0.51],[-0.051,0.5],[-0.1,0.49],[-0.149,0.49],[-0.19,0.47],[-0.24,0.45],[-0.28,0.42],[-0.33,0.4],[-0.359,0.36],[-0.39,0.32],[-0.43,0.28],[-0.45,0.24],[-0.47,0.19],[-0.48,0.15],[-0.5,0.1],[-0.51,0.05]],"v":[[0,15.651],[-1.54,15.571],[-3.061,15.341],[-4.54,14.971],[-5.99,14.451],[-7.37,13.801],[-8.69,13.011],[-9.92,12.101],[-11.07,11.071],[-12.1,9.921],[-13.01,8.691],[-13.8,7.371],[-14.45,5.981],[-14.97,4.541],[-15.35,3.061],[-15.57,1.541],[-15.65,0.001],[-15.57,-1.539],[-15.35,-3.059],[-14.97,-4.539],[-14.45,-5.989],[-13.8,-7.368],[-13.01,-8.689],[-12.1,-9.919],[-11.07,-11.069],[-9.92,-12.099],[-8.69,-13.009],[-7.37,-13.799],[-5.99,-14.449],[-4.55,-14.969],[-3.061,-15.349],[-1.54,-15.569],[1.54,-15.569],[3.06,-15.349],[4.54,-14.969],[5.979,-14.449],[7.37,-13.799],[8.689,-13.009],[9.92,-12.099],[11.06,-11.069],[12.1,-9.919],[13.01,-8.689],[13.8,-7.368],[14.45,-5.989],[14.97,-4.539],[15.34,-3.059],[15.57,-1.539],[15.65,0.001],[15.57,1.541],[15.34,3.061],[14.97,4.541],[14.45,5.981],[13.8,7.371],[13.01,8.691],[12.1,9.921],[11.06,11.071],[9.92,12.101],[8.689,13.011],[7.37,13.801],[5.979,14.451],[4.54,14.971],[3.06,15.341],[1.54,15.571]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('189-domain-verification-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[359.38,125.002],"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":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[8.644,0],[0,0],[0,8.643],[-8.644,0],[0,0],[0,-8.643]],"o":[[0,0],[-8.644,0],[0,-8.643],[0,0],[8.644,0],[0,8.643]],"v":[[171.876,15.65],[-171.876,15.65],[-187.526,0],[-171.876,-15.65],[171.876,-15.65],[187.526,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('189-domain-verification-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[250.004,171.878],"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 3","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[250.004,171.878],"ix":2},"a":{"a":0,"k":[250.004,171.878],"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":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[4.002,0],[3.056,3.051],[0,0],[-6.107,6.116],[-6.116,-6.108],[0,0],[0,0],[-6.107,-6.115],[6.115,-6.108],[0,0]],"o":[[-4.002,0],[0,0],[-6.115,-6.108],[6.108,-6.114],[0,0],[0,0],[6.115,-6.107],[6.108,6.115],[0,0],[-3.055,3.051]],"v":[[-23.984,61.442],[-35.043,56.866],[-78.768,13.195],[-78.782,-8.938],[-56.649,-8.951],[-23.984,23.673],[56.649,-56.865],[78.781,-56.852],[78.768,-34.72],[-12.924,56.865]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('189-domain-verification-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[250.003,292.753],"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,-2.858],[0,0],[-2.858,0],[0,0],[0,2.858],[0,0],[2.858,0]],"o":[[-2.858,0],[0,0],[0,2.858],[0,0],[2.858,0],[0,0],[0,-2.858],[0,0]],"v":[[-151.042,-156.226],[-156.226,-151.043],[-156.226,151.042],[-151.042,156.226],[151.042,156.226],[156.226,151.042],[156.226,-151.043],[151.042,-156.226]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[20.117,0],[0,0],[0,20.117],[0,0],[-20.117,0],[0,0],[0,-20.117],[0,0]],"o":[[0,0],[-20.117,0],[0,0],[0,-20.117],[0,0],[20.117,0],[0,0],[0,20.117]],"v":[[151.042,187.526],[-151.042,187.526],[-187.526,151.042],[-187.526,-151.043],[-151.042,-187.526],[151.042,-187.526],[187.526,-151.043],[187.526,151.042]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('189-domain-verification-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[250.004,250.003],"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},{"ty":"tr","p":{"a":0,"k":[250.004,250.003],"ix":2},"a":{"a":0,"k":[250.004,250.003],"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}],"ip":90,"op":300,"st":0,"bm":0},{"ddd":0,"ind":2,"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.004,250.003,0],"ix":2,"l":2},"a":{"a":0,"k":[250.004,250.003,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,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.51,0],[0.51,0.05],[0.5,0.1],[0.479,0.15],[0.47,0.2],[0.45,0.24],[0.42,0.28],[0.39,0.32],[0.36,0.36],[0.319,0.4],[0.279,0.42],[0.239,0.45],[0.19,0.47],[0.149,0.49],[0.1,0.5],[0.05,0.5],[0,0.51],[-0.06,0.51],[-0.101,0.5],[-0.15,0.49],[-0.2,0.47],[-0.24,0.45],[-0.28,0.42],[-0.32,0.39],[-0.359,0.36],[-0.4,0.32],[-0.42,0.28],[-0.45,0.24],[-0.47,0.19],[-0.48,0.15],[-0.5,0.1],[-0.51,0.05],[-1.021,-0.11],[-0.5,-0.1],[-0.48,-0.15],[-0.46,-0.2],[-0.45,-0.24],[-0.43,-0.29],[-0.39,-0.32],[-0.359,-0.36],[-0.33,-0.4],[-0.28,-0.43],[-0.24,-0.45],[-0.2,-0.47],[-0.149,-0.48],[-0.1,-0.5],[-0.051,-0.51],[0,-0.51],[0.05,-0.51],[0.1,-0.5],[0.141,-0.48],[0.189,-0.47],[0.23,-0.45],[0.28,-0.43],[0.32,-0.39],[0.37,-0.37],[0.4,-0.33],[0.421,-0.28],[0.45,-0.24],[0.48,-0.19],[0.49,-0.15],[0.5,-0.1],[0.51,-0.05]],"o":[[-0.51,0],[-0.51,-0.05],[-0.5,-0.1],[-0.49,-0.15],[-0.47,-0.19],[-0.45,-0.24],[-0.42,-0.28],[-0.4,-0.33],[-0.359,-0.37],[-0.32,-0.39],[-0.28,-0.43],[-0.24,-0.45],[-0.2,-0.47],[-0.15,-0.48],[-0.101,-0.5],[-0.06,-0.51],[0,-0.51],[0.05,-0.51],[0.1,-0.5],[0.149,-0.48],[0.19,-0.47],[0.239,-0.45],[0.279,-0.43],[0.319,-0.4],[0.36,-0.36],[0.39,-0.32],[0.42,-0.29],[0.45,-0.24],[0.47,-0.2],[0.489,-0.15],[0.5,-0.1],[1.02,-0.11],[0.51,0.05],[0.5,0.1],[0.49,0.15],[0.48,0.19],[0.45,0.24],[0.421,0.28],[0.4,0.32],[0.37,0.36],[0.32,0.39],[0.28,0.42],[0.23,0.45],[0.189,0.47],[0.141,0.49],[0.1,0.5],[0.05,0.51],[0,0.51],[-0.051,0.5],[-0.1,0.5],[-0.149,0.49],[-0.2,0.48],[-0.24,0.45],[-0.28,0.42],[-0.33,0.4],[-0.359,0.36],[-0.39,0.32],[-0.43,0.28],[-0.45,0.24],[-0.46,0.2],[-0.48,0.15],[-0.5,0.1],[-0.51,0.05]],"v":[[0,15.651],[-1.54,15.571],[-3.061,15.341],[-4.54,14.971],[-5.99,14.451],[-7.37,13.801],[-8.69,13.011],[-9.92,12.101],[-11.07,11.071],[-12.1,9.921],[-13.01,8.691],[-13.8,7.371],[-14.45,5.981],[-14.97,4.541],[-15.35,3.061],[-15.57,1.541],[-15.65,0.001],[-15.57,-1.539],[-15.35,-3.059],[-14.97,-4.549],[-14.45,-5.989],[-13.8,-7.368],[-13.01,-8.689],[-12.1,-9.919],[-11.07,-11.069],[-9.92,-12.099],[-8.69,-13.009],[-7.37,-13.799],[-5.99,-14.449],[-4.55,-14.969],[-3.061,-15.349],[-1.54,-15.569],[1.54,-15.569],[3.06,-15.349],[4.54,-14.969],[5.979,-14.449],[7.37,-13.799],[8.689,-13.009],[9.92,-12.099],[11.06,-11.069],[12.1,-9.919],[13.01,-8.689],[13.8,-7.368],[14.45,-5.989],[14.97,-4.549],[15.34,-3.059],[15.57,-1.539],[15.65,0.001],[15.57,1.541],[15.34,3.061],[14.97,4.541],[14.45,5.981],[13.8,7.371],[13.01,8.691],[12.1,9.921],[11.06,11.071],[9.92,12.101],[8.689,13.011],[7.37,13.801],[5.979,14.451],[4.54,14.971],[3.06,15.341],[1.54,15.571]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('189-domain-verification-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[296.88,125.002],"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.51,0],[0.51,0.05],[0.5,0.1],[0.479,0.15],[0.47,0.19],[0.45,0.24],[0.431,0.28],[0.39,0.32],[0.36,0.36],[0.319,0.4],[0.279,0.42],[0.239,0.45],[0.19,0.47],[0.149,0.49],[0.109,0.49],[0.05,0.5],[0,0.51],[-0.06,0.51],[-0.101,0.5],[-0.15,0.48],[-0.2,0.47],[-0.24,0.45],[-0.28,0.43],[-0.32,0.39],[-0.359,0.37],[-0.4,0.32],[-0.42,0.28],[-0.45,0.24],[-0.47,0.19],[-0.48,0.15],[-0.5,0.11],[-0.51,0.05],[-1.021,-0.11],[-0.5,-0.1],[-0.48,-0.15],[-0.47,-0.2],[-0.45,-0.24],[-0.43,-0.28],[-0.39,-0.32],[-0.359,-0.36],[-0.33,-0.4],[-0.28,-0.42],[-0.24,-0.45],[-0.19,-0.47],[-0.149,-0.49],[-0.1,-0.5],[-0.051,-0.51],[0,-0.51],[0.05,-0.51],[0.1,-0.5],[0.15,-0.49],[0.189,-0.47],[0.24,-0.45],[0.28,-0.43],[0.32,-0.39],[0.37,-0.37],[0.4,-0.33],[0.421,-0.28],[0.45,-0.24],[0.471,-0.19],[0.49,-0.15],[0.5,-0.1],[0.5,-0.05]],"o":[[-0.51,0],[-0.51,-0.05],[-0.5,-0.1],[-0.49,-0.15],[-0.47,-0.19],[-0.45,-0.24],[-0.42,-0.28],[-0.4,-0.33],[-0.359,-0.37],[-0.32,-0.39],[-0.28,-0.43],[-0.24,-0.45],[-0.2,-0.47],[-0.15,-0.49],[-0.101,-0.5],[-0.06,-0.51],[0,-0.51],[0.05,-0.51],[0.109,-0.5],[0.149,-0.49],[0.19,-0.47],[0.239,-0.45],[0.279,-0.42],[0.319,-0.4],[0.36,-0.36],[0.39,-0.32],[0.42,-0.28],[0.45,-0.24],[0.47,-0.2],[0.489,-0.15],[0.5,-0.1],[1.02,-0.11],[0.5,0.05],[0.5,0.11],[0.49,0.15],[0.471,0.19],[0.45,0.24],[0.421,0.28],[0.4,0.32],[0.37,0.37],[0.32,0.39],[0.28,0.43],[0.24,0.45],[0.189,0.47],[0.15,0.48],[0.1,0.5],[0.05,0.51],[0,0.51],[-0.051,0.5],[-0.1,0.49],[-0.149,0.49],[-0.19,0.47],[-0.24,0.45],[-0.28,0.42],[-0.33,0.4],[-0.359,0.36],[-0.39,0.32],[-0.43,0.28],[-0.45,0.24],[-0.47,0.19],[-0.48,0.15],[-0.5,0.1],[-0.51,0.05]],"v":[[0,15.651],[-1.54,15.571],[-3.061,15.341],[-4.54,14.971],[-5.99,14.451],[-7.37,13.801],[-8.69,13.011],[-9.92,12.101],[-11.07,11.071],[-12.1,9.921],[-13.01,8.691],[-13.8,7.371],[-14.45,5.981],[-14.97,4.541],[-15.35,3.061],[-15.57,1.541],[-15.65,0.001],[-15.57,-1.539],[-15.35,-3.059],[-14.97,-4.539],[-14.45,-5.989],[-13.8,-7.368],[-13.01,-8.689],[-12.1,-9.919],[-11.07,-11.069],[-9.92,-12.099],[-8.69,-13.009],[-7.37,-13.799],[-5.99,-14.449],[-4.55,-14.969],[-3.061,-15.349],[-1.54,-15.569],[1.54,-15.569],[3.06,-15.349],[4.54,-14.969],[5.979,-14.449],[7.37,-13.799],[8.689,-13.009],[9.92,-12.099],[11.06,-11.069],[12.1,-9.919],[13.01,-8.689],[13.8,-7.368],[14.45,-5.989],[14.97,-4.539],[15.34,-3.059],[15.57,-1.539],[15.65,0.001],[15.57,1.541],[15.34,3.061],[14.97,4.541],[14.45,5.981],[13.8,7.371],[13.01,8.691],[12.1,9.921],[11.06,11.071],[9.92,12.101],[8.689,13.011],[7.37,13.801],[5.979,14.451],[4.54,14.971],[3.06,15.341],[1.54,15.571]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('189-domain-verification-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[359.38,125.002],"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":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[8.644,0],[0,0],[0,8.643],[-8.644,0],[0,0],[0,-8.643]],"o":[[0,0],[-8.644,0],[0,-8.643],[0,0],[8.644,0],[0,8.643]],"v":[[171.876,15.65],[-171.876,15.65],[-187.526,0],[-171.876,-15.65],[171.876,-15.65],[187.526,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('189-domain-verification-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[250.004,171.878],"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 3","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[250.004,171.878],"ix":2},"a":{"a":0,"k":[250.004,171.878],"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":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[4.002,0],[3.056,3.051],[0,0],[-6.107,6.116],[-6.116,-6.108],[0,0],[0,0],[-6.107,-6.115],[6.115,-6.108],[0,0]],"o":[[-4.002,0],[0,0],[-6.115,-6.108],[6.108,-6.114],[0,0],[0,0],[6.115,-6.107],[6.108,6.115],[0,0],[-3.055,3.051]],"v":[[-23.984,61.442],[-35.043,56.866],[-78.768,13.195],[-78.782,-8.938],[-56.649,-8.951],[-23.984,23.673],[56.649,-56.865],[78.781,-56.852],[78.768,-34.72],[-12.924,56.865]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('189-domain-verification-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[250.003,292.753],"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,-2.858],[0,0],[-2.858,0],[0,0],[0,2.858],[0,0],[2.858,0]],"o":[[-2.858,0],[0,0],[0,2.858],[0,0],[2.858,0],[0,0],[0,-2.858],[0,0]],"v":[[-151.042,-156.226],[-156.226,-151.043],[-156.226,151.042],[-151.042,156.226],[151.042,156.226],[156.226,151.042],[156.226,-151.043],[151.042,-156.226]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[20.117,0],[0,0],[0,20.117],[0,0],[-20.117,0],[0,0],[0,-20.117],[0,0]],"o":[[0,0],[-20.117,0],[0,0],[0,-20.117],[0,0],[20.117,0],[0,0],[0,20.117]],"v":[[151.042,187.526],[-151.042,187.526],[-187.526,151.042],[-187.526,-151.043],[-151.042,-187.526],[151.042,-187.526],[187.526,-151.043],[187.526,151.042]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('189-domain-verification-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[250.004,250.003],"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},{"ty":"tr","p":{"a":0,"k":[250.004,250.003],"ix":2},"a":{"a":0,"k":[250.004,250.003],"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}],"ip":0,"op":1,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".primary.design","cl":"primary design","parent":6,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,42.749,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,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[-81.604,66.643],[-34.5,-33],[34.5,-35.5]],"o":[[0,0],[0,0],[0,0],[90,-73.5],[46.014,44.014],[0,0]],"v":[[67.709,-45.792],[-23.984,45.792],[-67.709,2.122],[-118.504,-193.753],[116.997,-195.253],[67.997,-45.753]],"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.333],"y":[0]},"t":0,"s":[22.5]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":15,"s":[28.5]},{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":60,"s":[28.5]},{"t":90,"s":[22.5]}],"ix":2},"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":15,"s":[181]},{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":60,"s":[181]},{"t":90,"s":[360]}],"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('189-domain-verification-outline').layer('Color & Stroke Change').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":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"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":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":90,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":".primary.design","cl":"primary design","td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250.004,250.003,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,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":1,"s":[{"i":[[11.505,0],[0,0],[0,-11.506],[0,0],[-11.506,0],[0,0],[0,11.505],[0,0]],"o":[[0,0],[-11.506,0],[0,0],[0,11.505],[0,0],[11.505,0],[0,0],[0,-11.506]],"v":[[151.042,-171.876],[-151.042,-171.876],[-171.876,-151.042],[-171.876,151.042],[-151.042,171.876],[151.042,171.876],[171.876,151.042],[171.876,-151.042]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.333,"y":0},"t":15,"s":[{"i":[[107.136,0],[0,0],[0,-106.591],[0,0],[-106.043,0],[0,0],[0,106.032],[0,0]],"o":[[0,0],[-106.591,0],[0,0],[0,106.031],[0,0],[106.032,0],[0,0],[0,-107.148]],"v":[[-1.004,-192.997],[-0.004,-192.997],[-192.997,-0.004],[-192.997,0.997],[-0.997,192.997],[0.996,192.997],[192.997,0.996],[192.997,1.004]],"c":true}]},{"i":{"x":0.2,"y":1},"o":{"x":0.167,"y":0},"t":60,"s":[{"i":[[107.136,0],[0,0],[0,-106.591],[0,0],[-106.043,0],[0,0],[0,106.032],[0,0]],"o":[[0,0],[-106.591,0],[0,0],[0,106.031],[0,0],[106.032,0],[0,0],[0,-107.148]],"v":[[-1.004,-192.997],[-0.004,-192.997],[-192.997,-0.004],[-192.997,0.997],[-0.997,192.997],[0.996,192.997],[192.997,0.996],[192.997,1.004]],"c":true}]},{"t":90,"s":[{"i":[[11.505,0],[0,0],[0,-11.506],[0,0],[-11.506,0],[0,0],[0,11.505],[0,0]],"o":[[0,0],[-11.506,0],[0,0],[0,11.505],[0,0],[11.505,0],[0,0],[0,-11.506]],"v":[[151.042,-171.876],[-151.042,-171.876],[-171.876,-151.042],[-171.876,151.042],[-151.042,171.876],[151.042,171.876],[171.876,151.042],[171.876,-151.042]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"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":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":90,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":".primary.design","cl":"primary design","tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":1,"s":[250.004,148.439,0],"to":[0,-32.333,0],"ti":[0,32.333,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.333,"y":0.333},"t":15,"s":[250.004,-45.561,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.2,"y":1},"o":{"x":0.333,"y":0},"t":60,"s":[250.004,-45.561,0],"to":[0,32.333,0],"ti":[0,-32.333,0]},{"t":90,"s":[250.004,148.439,0]}],"ix":2,"l":2},"a":{"a":0,"k":[250.004,148.439,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,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.51,0],[0.51,0.05],[0.5,0.1],[0.479,0.15],[0.47,0.2],[0.45,0.24],[0.42,0.28],[0.39,0.32],[0.36,0.36],[0.319,0.4],[0.279,0.42],[0.239,0.45],[0.19,0.47],[0.149,0.49],[0.1,0.5],[0.05,0.5],[0,0.51],[-0.06,0.51],[-0.101,0.5],[-0.15,0.49],[-0.2,0.47],[-0.24,0.45],[-0.28,0.42],[-0.32,0.39],[-0.359,0.36],[-0.4,0.32],[-0.42,0.28],[-0.45,0.24],[-0.47,0.19],[-0.48,0.15],[-0.5,0.1],[-0.51,0.05],[-1.021,-0.11],[-0.5,-0.1],[-0.48,-0.15],[-0.46,-0.2],[-0.45,-0.24],[-0.43,-0.29],[-0.39,-0.32],[-0.359,-0.36],[-0.33,-0.4],[-0.28,-0.43],[-0.24,-0.45],[-0.2,-0.47],[-0.149,-0.48],[-0.1,-0.5],[-0.051,-0.51],[0,-0.51],[0.05,-0.51],[0.1,-0.5],[0.141,-0.48],[0.189,-0.47],[0.23,-0.45],[0.28,-0.43],[0.32,-0.39],[0.37,-0.37],[0.4,-0.33],[0.421,-0.28],[0.45,-0.24],[0.48,-0.19],[0.49,-0.15],[0.5,-0.1],[0.51,-0.05]],"o":[[-0.51,0],[-0.51,-0.05],[-0.5,-0.1],[-0.49,-0.15],[-0.47,-0.19],[-0.45,-0.24],[-0.42,-0.28],[-0.4,-0.33],[-0.359,-0.37],[-0.32,-0.39],[-0.28,-0.43],[-0.24,-0.45],[-0.2,-0.47],[-0.15,-0.48],[-0.101,-0.5],[-0.06,-0.51],[0,-0.51],[0.05,-0.51],[0.1,-0.5],[0.149,-0.48],[0.19,-0.47],[0.239,-0.45],[0.279,-0.43],[0.319,-0.4],[0.36,-0.36],[0.39,-0.32],[0.42,-0.29],[0.45,-0.24],[0.47,-0.2],[0.489,-0.15],[0.5,-0.1],[1.02,-0.11],[0.51,0.05],[0.5,0.1],[0.49,0.15],[0.48,0.19],[0.45,0.24],[0.421,0.28],[0.4,0.32],[0.37,0.36],[0.32,0.39],[0.28,0.42],[0.23,0.45],[0.189,0.47],[0.141,0.49],[0.1,0.5],[0.05,0.51],[0,0.51],[-0.051,0.5],[-0.1,0.5],[-0.149,0.49],[-0.2,0.48],[-0.24,0.45],[-0.28,0.42],[-0.33,0.4],[-0.359,0.36],[-0.39,0.32],[-0.43,0.28],[-0.45,0.24],[-0.46,0.2],[-0.48,0.15],[-0.5,0.1],[-0.51,0.05]],"v":[[0,15.651],[-1.54,15.571],[-3.061,15.341],[-4.54,14.971],[-5.99,14.451],[-7.37,13.801],[-8.69,13.011],[-9.92,12.101],[-11.07,11.071],[-12.1,9.921],[-13.01,8.691],[-13.8,7.371],[-14.45,5.981],[-14.97,4.541],[-15.35,3.061],[-15.57,1.541],[-15.65,0.001],[-15.57,-1.539],[-15.35,-3.059],[-14.97,-4.549],[-14.45,-5.989],[-13.8,-7.368],[-13.01,-8.689],[-12.1,-9.919],[-11.07,-11.069],[-9.92,-12.099],[-8.69,-13.009],[-7.37,-13.799],[-5.99,-14.449],[-4.55,-14.969],[-3.061,-15.349],[-1.54,-15.569],[1.54,-15.569],[3.06,-15.349],[4.54,-14.969],[5.979,-14.449],[7.37,-13.799],[8.689,-13.009],[9.92,-12.099],[11.06,-11.069],[12.1,-9.919],[13.01,-8.689],[13.8,-7.368],[14.45,-5.989],[14.97,-4.549],[15.34,-3.059],[15.57,-1.539],[15.65,0.001],[15.57,1.541],[15.34,3.061],[14.97,4.541],[14.45,5.981],[13.8,7.371],[13.01,8.691],[12.1,9.921],[11.06,11.071],[9.92,12.101],[8.689,13.011],[7.37,13.801],[5.979,14.451],[4.54,14.971],[3.06,15.341],[1.54,15.571]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('189-domain-verification-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[296.88,125.002],"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.51,0],[0.51,0.05],[0.5,0.1],[0.479,0.15],[0.47,0.19],[0.45,0.24],[0.431,0.28],[0.39,0.32],[0.36,0.36],[0.319,0.4],[0.279,0.42],[0.239,0.45],[0.19,0.47],[0.149,0.49],[0.109,0.49],[0.05,0.5],[0,0.51],[-0.06,0.51],[-0.101,0.5],[-0.15,0.48],[-0.2,0.47],[-0.24,0.45],[-0.28,0.43],[-0.32,0.39],[-0.359,0.37],[-0.4,0.32],[-0.42,0.28],[-0.45,0.24],[-0.47,0.19],[-0.48,0.15],[-0.5,0.11],[-0.51,0.05],[-1.021,-0.11],[-0.5,-0.1],[-0.48,-0.15],[-0.47,-0.2],[-0.45,-0.24],[-0.43,-0.28],[-0.39,-0.32],[-0.359,-0.36],[-0.33,-0.4],[-0.28,-0.42],[-0.24,-0.45],[-0.19,-0.47],[-0.149,-0.49],[-0.1,-0.5],[-0.051,-0.51],[0,-0.51],[0.05,-0.51],[0.1,-0.5],[0.15,-0.49],[0.189,-0.47],[0.24,-0.45],[0.28,-0.43],[0.32,-0.39],[0.37,-0.37],[0.4,-0.33],[0.421,-0.28],[0.45,-0.24],[0.471,-0.19],[0.49,-0.15],[0.5,-0.1],[0.5,-0.05]],"o":[[-0.51,0],[-0.51,-0.05],[-0.5,-0.1],[-0.49,-0.15],[-0.47,-0.19],[-0.45,-0.24],[-0.42,-0.28],[-0.4,-0.33],[-0.359,-0.37],[-0.32,-0.39],[-0.28,-0.43],[-0.24,-0.45],[-0.2,-0.47],[-0.15,-0.49],[-0.101,-0.5],[-0.06,-0.51],[0,-0.51],[0.05,-0.51],[0.109,-0.5],[0.149,-0.49],[0.19,-0.47],[0.239,-0.45],[0.279,-0.42],[0.319,-0.4],[0.36,-0.36],[0.39,-0.32],[0.42,-0.28],[0.45,-0.24],[0.47,-0.2],[0.489,-0.15],[0.5,-0.1],[1.02,-0.11],[0.5,0.05],[0.5,0.11],[0.49,0.15],[0.471,0.19],[0.45,0.24],[0.421,0.28],[0.4,0.32],[0.37,0.37],[0.32,0.39],[0.28,0.43],[0.24,0.45],[0.189,0.47],[0.15,0.48],[0.1,0.5],[0.05,0.51],[0,0.51],[-0.051,0.5],[-0.1,0.49],[-0.149,0.49],[-0.19,0.47],[-0.24,0.45],[-0.28,0.42],[-0.33,0.4],[-0.359,0.36],[-0.39,0.32],[-0.43,0.28],[-0.45,0.24],[-0.47,0.19],[-0.48,0.15],[-0.5,0.1],[-0.51,0.05]],"v":[[0,15.651],[-1.54,15.571],[-3.061,15.341],[-4.54,14.971],[-5.99,14.451],[-7.37,13.801],[-8.69,13.011],[-9.92,12.101],[-11.07,11.071],[-12.1,9.921],[-13.01,8.691],[-13.8,7.371],[-14.45,5.981],[-14.97,4.541],[-15.35,3.061],[-15.57,1.541],[-15.65,0.001],[-15.57,-1.539],[-15.35,-3.059],[-14.97,-4.539],[-14.45,-5.989],[-13.8,-7.368],[-13.01,-8.689],[-12.1,-9.919],[-11.07,-11.069],[-9.92,-12.099],[-8.69,-13.009],[-7.37,-13.799],[-5.99,-14.449],[-4.55,-14.969],[-3.061,-15.349],[-1.54,-15.569],[1.54,-15.569],[3.06,-15.349],[4.54,-14.969],[5.979,-14.449],[7.37,-13.799],[8.689,-13.009],[9.92,-12.099],[11.06,-11.069],[12.1,-9.919],[13.01,-8.689],[13.8,-7.368],[14.45,-5.989],[14.97,-4.539],[15.34,-3.059],[15.57,-1.539],[15.65,0.001],[15.57,1.541],[15.34,3.061],[14.97,4.541],[14.45,5.981],[13.8,7.371],[13.01,8.691],[12.1,9.921],[11.06,11.071],[9.92,12.101],[8.689,13.011],[7.37,13.801],[5.979,14.451],[4.54,14.971],[3.06,15.341],[1.54,15.571]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('189-domain-verification-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[359.38,125.002],"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":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[8.644,0],[0,0],[0,8.643],[-8.644,0],[0,0],[0,-8.643]],"o":[[0,0],[-8.644,0],[0,-8.643],[0,0],[8.644,0],[0,8.643]],"v":[[171.876,15.65],[-171.876,15.65],[-187.526,0],[-171.876,-15.65],[171.876,-15.65],[187.526,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('189-domain-verification-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[250.004,171.878],"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 3","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[250.004,171.878],"ix":2},"a":{"a":0,"k":[250.004,171.878],"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":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":90,"st":1,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"t":1,"s":[0]},{"t":60,"s":[360]}],"ix":10},"p":{"a":0,"k":[250.004,250.003,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,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":1,"s":[{"i":[[11.505,0],[0,0],[0,-11.506],[0,0],[-11.506,0],[0,0],[0,11.505],[0,0]],"o":[[0,0],[-11.506,0],[0,0],[0,11.505],[0,0],[11.505,0],[0,0],[0,-11.506]],"v":[[151.042,-171.876],[-151.042,-171.876],[-171.876,-151.042],[-171.876,151.042],[-151.042,171.876],[151.042,171.876],[171.876,151.042],[171.876,-151.042]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":15,"s":[{"i":[[107.136,0],[0,0],[0,-106.591],[0,0],[-106.043,0],[0,0],[0,106.032],[0,0]],"o":[[0,0],[-106.591,0],[0,0],[0,106.031],[0,0],[106.032,0],[0,0],[0,-107.148]],"v":[[-1.004,-192.997],[-0.004,-192.997],[-192.997,-0.004],[-192.997,0.997],[-0.997,192.997],[0.996,192.997],[192.997,0.996],[192.997,1.004]],"c":true}]},{"i":{"x":0.2,"y":1},"o":{"x":0.167,"y":0.167},"t":60,"s":[{"i":[[107.136,0],[0,0],[0,-106.591],[0,0],[-106.043,0],[0,0],[0,106.032],[0,0]],"o":[[0,0],[-106.591,0],[0,0],[0,106.031],[0,0],[106.032,0],[0,0],[0,-107.148]],"v":[[-1.004,-192.997],[-0.004,-192.997],[-192.997,-0.004],[-192.997,0.997],[-0.997,192.997],[0.996,192.997],[192.997,0.996],[192.997,1.004]],"c":true}]},{"t":90,"s":[{"i":[[11.505,0],[0,0],[0,-11.506],[0,0],[-11.506,0],[0,0],[0,11.505],[0,0]],"o":[[0,0],[-11.506,0],[0,0],[0,11.505],[0,0],[11.505,0],[0,0],[0,-11.506]],"v":[[151.042,-171.876],[-151.042,-171.876],[-171.876,-151.042],[-171.876,151.042],[-151.042,171.876],[151.042,171.876],[171.876,151.042],[171.876,-151.042]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":7,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":9,"s":[7]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":14,"s":[0]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.299],"y":[0]},"t":60,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":70,"s":[0]},{"t":72,"s":[0]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":7,"s":[100]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":14,"s":[66.5]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0]},"t":60,"s":[66.5]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":70,"s":[89.5]},{"t":72,"s":[100]}],"ix":2},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"t":60,"s":[60]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":70,"s":[16]},{"t":72,"s":[0]}],"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('189-domain-verification-outline').layer('Color & Stroke Change').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":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"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":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":90,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"lordicon.com Outlines","cl":"com","sr":1,"ks":{"o":{"a":0,"k":20,"ix":11,"x":"var $bm_rt;\nvar checkbox = thisComp.layer('02092020').effect('02092020002')('Checkbox');\nif (checkbox == 1) {\n $bm_rt = 20;\n} else {\n $bm_rt = 0;\n}\n;"},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[249.934,481.369,0],"ix":2,"l":2},"a":{"a":0,"k":[79.934,0.369,0],"ix":1,"l":2},"s":{"a":0,"k":[265.159,265.159,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],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[1.415,0],[11.014,0],[11.014,-2.523],[4.656,-2.523],[4.656,-14.809],[1.415,-14.809]],"c":true},"ix":2},"nm":"l","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"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":"l","np":3,"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,-3.938],[-1.62,-1.723],[-1.949,0],[-1.641,1.846],[0,2.154],[1.579,1.805],[1.579,0]],"o":[[0,1.354],[1.354,1.415],[1.231,0],[1.21,-1.354],[0,-1.456],[-1.456,-1.641],[-5.333,0]],"v":[[11.167,-7.199],[12.992,-1.661],[18.243,0.369],[23.514,-1.743],[25.381,-7.548],[23.494,-13.127],[18.284,-15.137]],"c":true},"ix":2},"nm":"o","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,1.415],[-0.841,1.026],[-1.19,0],[-0.615,-1.825],[0,-0.718],[0.492,-0.738],[1.292,0],[0.451,0.615]],"o":[[0,-1.682],[0.595,-0.759],[1.518,0],[0.308,0.902],[0,2.359],[-0.595,0.923],[-1.477,0],[-0.882,-1.149]],"v":[[14.49,-7.302],[15.577,-11.609],[18.305,-12.86],[21.689,-10.235],[22.058,-7.589],[21.053,-3.343],[18.284,-1.969],[15.597,-3.159]],"c":true},"ix":2},"nm":"o","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"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":"o","np":5,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[-0.287,-0.841],[-0.144,-0.82],[0,0],[0.164,0.656],[0.226,1.743],[2.236,0.205],[0,2.769],[0.923,0.8],[1.641,-0.021],[0,0]],"o":[[0,0],[0,0],[0,0],[0.533,0],[0.205,0.574],[0,0],[-0.164,-0.246],[-0.103,-0.41],[-0.267,-1.928],[0.718,-0.205],[0,-0.964],[-1.19,-1.026],[0,0],[0,0]],"v":[[27.381,0],[30.622,0],[30.622,-5.989],[33.411,-5.989],[35.011,-5.148],[35.811,0],[39.318,0],[38.867,-1.067],[38.416,-3.938],[35.749,-7.343],[38.847,-10.973],[37.554,-13.824],[33.063,-14.829],[27.381,-14.829]],"c":true},"ix":2},"nm":"r","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[-0.492,-0.349],[0,-1.005],[0.226,-0.164],[0.369,0],[0,0]],"o":[[0,0],[1.005,0],[0.287,0.185],[0,1.046],[-0.513,0.41],[0,0],[0,0]],"v":[[30.519,-12.491],[32.652,-12.491],[34.744,-12.142],[35.524,-10.481],[34.703,-8.758],[33.083,-8.348],[30.519,-8.348]],"c":true},"ix":2},"nm":"r","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"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":"r","np":5,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[-0.554,0.103],[0,4.553],[1.866,1.374],[0.82,0],[0,0]],"o":[[0,0],[1.497,0],[2.81,-0.513],[0,-2.113],[-1.784,-1.313],[0,0],[0,0]],"v":[[41.068,0],[45.683,0],[48.349,-0.164],[53.6,-7.609],[51.077,-13.434],[45.97,-14.768],[41.068,-14.788]],"c":true},"ix":2},"nm":"d","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[-0.656,-0.185],[0,-2.092],[1.251,-1.251],[1.354,0],[0.349,0.021]],"o":[[1.825,-0.082],[1.99,0.554],[0,0.718],[-0.923,0.923],[-0.369,0],[0,0]],"v":[[44.288,-12.388],[47.611,-12.183],[50.318,-7.609],[48.985,-3.425],[45.539,-2.4],[44.288,-2.441]],"c":true},"ix":2},"nm":"d","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"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":"d","np":5,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[55.669,0],[58.849,0],[58.849,-14.87],[55.669,-14.87]],"c":true},"ix":2},"nm":"i","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"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":"i","np":3,"cix":2,"bm":0,"ix":5,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[3.241,0],[0,-4.697],[-5.107,0],[-1.313,1.354],[-0.062,0.882],[0,0],[1.333,0],[0,0.882],[-2.359,0],[-0.062,-0.513]],"o":[[0,-2.954],[-4.164,0],[0,3.671],[1.354,0],[1.19,-1.231],[0,0],[-0.062,1.969],[-3.097,0],[0,-3.056],[2.154,0],[0,0]],"v":[[73.104,-9.989],[67.587,-14.911],[60.798,-7.097],[67.566,0.349],[71.894,-1.313],[73.227,-4.799],[69.884,-4.799],[67.218,-1.99],[64.121,-7.076],[67.464,-12.593],[69.864,-9.989]],"c":true},"ix":2},"nm":"c","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"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":"c","np":3,"cix":2,"bm":0,"ix":6,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-3.938],[-1.62,-1.723],[-1.949,0],[-1.641,1.846],[0,2.154],[1.579,1.805],[1.579,0]],"o":[[0,1.354],[1.354,1.415],[1.231,0],[1.21,-1.354],[0,-1.456],[-1.456,-1.641],[-5.333,0]],"v":[[74.546,-7.199],[76.372,-1.661],[81.622,0.369],[86.894,-1.743],[88.76,-7.548],[86.873,-13.127],[81.663,-15.137]],"c":true},"ix":2},"nm":"o","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,1.415],[-0.841,1.026],[-1.19,0],[-0.615,-1.825],[0,-0.718],[0.492,-0.738],[1.292,0],[0.451,0.615]],"o":[[0,-1.682],[0.595,-0.759],[1.518,0],[0.308,0.902],[0,2.359],[-0.595,0.923],[-1.477,0],[-0.882,-1.149]],"v":[[77.869,-7.302],[78.956,-11.609],[81.684,-12.86],[85.068,-10.235],[85.437,-7.589],[84.432,-3.343],[81.663,-1.969],[78.977,-3.159]],"c":true},"ix":2},"nm":"o","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"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":"o","np":5,"cix":2,"bm":0,"ix":7,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[91.007,0],[94.001,0],[94.001,-12.306],[99.744,0],[104.113,0],[104.113,-14.829],[101.159,-14.829],[101.159,-3.159],[95.601,-14.829],[91.007,-14.829]],"c":true},"ix":2},"nm":"n","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"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":"n","np":3,"cix":2,"bm":0,"ix":8,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[106.893,0],[109.497,0],[109.497,-2.728],[106.893,-2.728]],"c":true},"ix":2},"nm":".","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"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":".","np":3,"cix":2,"bm":0,"ix":9,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[3.241,0],[0,-4.697],[-5.107,0],[-1.313,1.354],[-0.062,0.882],[0,0],[1.333,0],[0,0.882],[-2.359,0],[-0.062,-0.513]],"o":[[0,-2.954],[-4.164,0],[0,3.671],[1.354,0],[1.19,-1.231],[0,0],[-0.062,1.969],[-3.097,0],[0,-3.056],[2.154,0],[0,0]],"v":[[124.04,-9.989],[118.523,-14.911],[111.734,-7.097],[118.502,0.349],[122.83,-1.313],[124.163,-4.799],[120.82,-4.799],[118.154,-1.99],[115.057,-7.076],[118.4,-12.593],[120.8,-9.989]],"c":true},"ix":2},"nm":"c","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"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":"c","np":3,"cix":2,"bm":0,"ix":10,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-3.938],[-1.62,-1.723],[-1.949,0],[-1.641,1.846],[0,2.154],[1.579,1.805],[1.579,0]],"o":[[0,1.354],[1.354,1.415],[1.231,0],[1.21,-1.354],[0,-1.456],[-1.456,-1.641],[-5.333,0]],"v":[[125.482,-7.199],[127.308,-1.661],[132.558,0.369],[137.829,-1.743],[139.696,-7.548],[137.809,-13.127],[132.599,-15.137]],"c":true},"ix":2},"nm":"o","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,1.415],[-0.841,1.026],[-1.19,0],[-0.615,-1.825],[0,-0.718],[0.492,-0.738],[1.292,0],[0.451,0.615]],"o":[[0,-1.682],[0.595,-0.759],[1.518,0],[0.308,0.902],[0,2.359],[-0.595,0.923],[-1.477,0],[-0.882,-1.149]],"v":[[128.805,-7.302],[129.892,-11.609],[132.62,-12.86],[136.004,-10.235],[136.373,-7.589],[135.368,-3.343],[132.599,-1.969],[129.912,-3.159]],"c":true},"ix":2},"nm":"o","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"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":"o","np":5,"cix":2,"bm":0,"ix":11,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[141.696,0],[144.67,0],[144.67,-12.716],[148.629,0],[151.254,0],[155.295,-12.716],[155.295,0],[158.453,0],[158.453,-14.829],[153.408,-14.829],[150.024,-4.041],[146.885,-14.829],[141.696,-14.829]],"c":true},"ix":2},"nm":"m","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"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":"m","np":3,"cix":2,"bm":0,"ix":12,"mn":"ADBE Vector Group","hd":false}],"ip":2.5,"op":25,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":3,"nm":"02092020","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-105,15,0],"ix":2,"l":2},"a":{"a":0,"k":[60,60,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"ef":[{"ty":5,"nm":"02092020002","np":3,"mn":"ADBE Checkbox Control","ix":1,"en":1,"ef":[{"ty":7,"nm":"Checkbox","mn":"ADBE Checkbox Control-0001","ix":1,"v":{"a":0,"k":0,"ix":1}}]}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":3,"nm":"Color & Stroke Change","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-55,-102,0],"ix":2,"l":2,"x":"var $bm_rt;\n$bm_rt = effect('Axis')('Point');"},"a":{"a":0,"k":[50,50,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2,"x":"var $bm_rt;\nvar temp;\ntemp = effect('Scale')('Slider');\n$bm_rt = [\n temp,\n temp\n];"}},"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}}]},{"ty":5,"nm":"Axis","np":3,"mn":"ADBE Point Control","ix":2,"en":1,"ef":[{"ty":3,"nm":"Point","mn":"ADBE Point Control-0001","ix":1,"v":{"a":0,"k":[250,250],"ix":1}}]},{"ty":5,"nm":"Scale","np":3,"mn":"ADBE Slider Control","ix":3,"en":1,"ef":[{"ty":0,"nm":"Slider","mn":"ADBE Slider Control-0001","ix":1,"v":{"a":0,"k":100,"ix":1}}]},{"ty":5,"nm":"State-Intro","np":3,"mn":"ADBE Slider Control","ix":4,"en":1,"ef":[{"ty":0,"nm":"Slider","mn":"ADBE Slider Control-0001","ix":1,"v":{"a":0,"k":0,"ix":1}}]},{"ty":5,"nm":"State-Hover","np":3,"mn":"ADBE Slider Control","ix":5,"en":1,"ef":[{"ty":0,"nm":"Slider","mn":"ADBE Slider Control-0001","ix":1,"v":{"a":0,"k":1,"ix":1}}]}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":0,"nm":"in-domain","parent":3,"refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11,"x":"var $bm_rt;\n$bm_rt = $bm_mul(thisComp.layer('Color & Stroke Change').effect('State-Intro')('Slider'), 100);"},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[50,50,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":101,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":0,"nm":"hover-domain","parent":3,"refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11,"x":"var $bm_rt;\n$bm_rt = $bm_mul(thisComp.layer('Color & Stroke Change').effect('State-Hover')('Slider'), 100);"},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[50,50,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":101,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/frontend/src/components/navigation/NavHeader.tsx b/frontend/src/components/navigation/NavHeader.tsx index 866010c21..d2a046139 100644 --- a/frontend/src/components/navigation/NavHeader.tsx +++ b/frontend/src/components/navigation/NavHeader.tsx @@ -1,6 +1,6 @@ import Link from "next/link"; import { useRouter } from "next/router"; -import { faAngleRight } from "@fortawesome/free-solid-svg-icons"; +import { faAngleRight, faLock } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useOrganization, useWorkspace } from "@app/context"; @@ -16,6 +16,8 @@ type Props = { onEnvChange?: (slug: string) => void; secretPath?: string; isFolderMode?: boolean; + isProtectedBranch?: boolean; + protectionPolicyName?: string; }; // TODO: make links clickable and clean up @@ -42,7 +44,9 @@ export default function NavHeader({ userAvailableEnvs = [], onEnvChange, isFolderMode, - secretPath = "/" + secretPath = "/", + isProtectedBranch = false, + protectionPolicyName }: Props): JSX.Element { const { currentWorkspace } = useWorkspace(); const { currentOrg } = useOrganization(); @@ -151,6 +155,11 @@ export default function NavHeader({ ); })} + {isProtectedBranch && ( + + + + )} ); } diff --git a/frontend/src/components/v2/Button/Button.tsx b/frontend/src/components/v2/Button/Button.tsx index 7c2a5fd6d..63a735d65 100644 --- a/frontend/src/components/v2/Button/Button.tsx +++ b/frontend/src/components/v2/Button/Button.tsx @@ -11,6 +11,9 @@ type Props = { isLoading?: boolean; }; +// refactor(akhilmhdh): both color and size variants are together need to split it +// colorSchema should handle all color class names +// variant should handle how the button padding and other types should be set const buttonVariants = cva( [ "button", @@ -67,7 +70,7 @@ const buttonVariants = cva( { colorSchema: "primary", variant: "solid", - className: "bg-primary-500 bg-opacity-90 hover:bg-primary-500 hover:text-black" + className: "text-black bg-primary-500 bg-opacity-90 hover:bg-primary-500 hover:text-black" }, { colorSchema: "primary", @@ -106,6 +109,12 @@ const buttonVariants = cva( variant: "outline", className: "text-red hover:bg-red hover:text-black" }, + { + colorSchema: "danger", + variant: "outline_bg", + className: + "bg-mineshaft-600 border border-red-500 hover:bg-red/[0.1] hover:border-red/40 text-red-500" + }, { colorSchema: "primary", variant: "plain", diff --git a/frontend/src/context/ProjectPermissionContext/types.ts b/frontend/src/context/ProjectPermissionContext/types.ts index 9a2e89713..ae4b49d63 100644 --- a/frontend/src/context/ProjectPermissionContext/types.ts +++ b/frontend/src/context/ProjectPermissionContext/types.ts @@ -20,7 +20,8 @@ export enum ProjectPermissionSub { IpAllowList = "ip-allowlist", Workspace = "workspace", Secrets = "secrets", - SecretRollback = "secret-rollback" + SecretRollback = "secret-rollback", + SecretApproval = "secret-approval" } type SubjectFields = { @@ -43,6 +44,7 @@ export type ProjectPermissionSet = | [ProjectPermissionActions, ProjectPermissionSub.IpAllowList] | [ProjectPermissionActions, ProjectPermissionSub.Settings] | [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens] + | [ProjectPermissionActions, ProjectPermissionSub.SecretApproval] | [ProjectPermissionActions.Delete, ProjectPermissionSub.Workspace] | [ProjectPermissionActions.Edit, ProjectPermissionSub.Workspace] | [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback] diff --git a/frontend/src/hooks/api/index.tsx b/frontend/src/hooks/api/index.tsx index 05de5a8d5..b065b23a1 100644 --- a/frontend/src/hooks/api/index.tsx +++ b/frontend/src/hooks/api/index.tsx @@ -8,6 +8,7 @@ export * from "./keys"; export * from "./organization"; export * from "./roles"; export * from "./secretApproval"; +export * from "./secretApprovalRequest"; export * from "./secretFolders"; export * from "./secretImports"; export * from "./secrets"; diff --git a/frontend/src/hooks/api/secretApproval/index.tsx b/frontend/src/hooks/api/secretApproval/index.tsx index 1d4353d9e..41774b9d1 100644 --- a/frontend/src/hooks/api/secretApproval/index.tsx +++ b/frontend/src/hooks/api/secretApproval/index.tsx @@ -3,4 +3,4 @@ export { useDeleteSecretApprovalPolicy, useUpdateSecretApprovalPolicy } from "./mutation"; -export { useGetSecretApprovalPolicies } from "./queries"; +export { useGetSecretApprovalPolicies, useGetSecretApprovalPolicyOfABoard } from "./queries"; diff --git a/frontend/src/hooks/api/secretApproval/mutation.tsx b/frontend/src/hooks/api/secretApproval/mutation.tsx index f171636a0..e0a8df95d 100644 --- a/frontend/src/hooks/api/secretApproval/mutation.tsx +++ b/frontend/src/hooks/api/secretApproval/mutation.tsx @@ -9,13 +9,14 @@ export const useCreateSecretApprovalPolicy = () => { const queryClient = useQueryClient(); return useMutation<{}, {}, TCreateSecretPolicyDTO>({ - mutationFn: async ({ environment, workspaceId, approvals, approvers, secretPath }) => { + mutationFn: async ({ environment, workspaceId, approvals, approvers, secretPath, name }) => { const { data } = await apiRequest.post("/api/v1/secret-approvals", { environment, workspaceId, approvals, approvers, - secretPath + secretPath, + name }); return data; }, @@ -29,11 +30,12 @@ export const useUpdateSecretApprovalPolicy = () => { const queryClient = useQueryClient(); return useMutation<{}, {}, TUpdateSecretPolicyDTO>({ - mutationFn: async ({ id, approvers, approvals, secretPath }) => { + mutationFn: async ({ id, approvers, approvals, secretPath, name }) => { const { data } = await apiRequest.patch(`/api/v1/secret-approvals/${id}`, { approvals, approvers, - secretPath + secretPath, + name }); return data; }, diff --git a/frontend/src/hooks/api/secretApproval/queries.tsx b/frontend/src/hooks/api/secretApproval/queries.tsx index 6c176a27a..ae90ff766 100644 --- a/frontend/src/hooks/api/secretApproval/queries.tsx +++ b/frontend/src/hooks/api/secretApproval/queries.tsx @@ -2,11 +2,19 @@ import { useQuery, UseQueryOptions } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; -import { TSecretApprovalPolicy } from "./types"; +import { + TGetSecretApprovalPoliciesDTO, + TGetSecretApprovalPolicyOfBoardDTO, + TSecretApprovalPolicy +} from "./types"; export const secretApprovalKeys = { getApprovalPolicies: (workspaceId: string) => - [{ workspaceId }, "secret-approval-policies"] as const + [{ workspaceId }, "secret-approval-policies"] as const, + getApprovalPolicyOfABoard: (workspaceId: string, environment: string, secretPath: string) => [ + { workspaceId, environment, secretPath }, + "Secret-approval-policy" + ] }; const fetchApprovalPolicies = async (workspaceId: string) => { @@ -20,7 +28,7 @@ const fetchApprovalPolicies = async (workspaceId: string) => { export const useGetSecretApprovalPolicies = ({ workspaceId, options = {} -}: { workspaceId: string } & { +}: TGetSecretApprovalPoliciesDTO & { options?: UseQueryOptions< TSecretApprovalPolicy[], unknown, @@ -34,3 +42,35 @@ export const useGetSecretApprovalPolicies = ({ ...options, enabled: Boolean(workspaceId) && (options?.enabled ?? true) }); + +const fetchApprovalPolicyOfABoard = async ( + workspaceId: string, + environment: string, + secretPath: string +) => { + const { data } = await apiRequest.get<{ policy: TSecretApprovalPolicy }>( + "/api/v1/secret-approvals/board", + { params: { workspaceId, environment, secretPath } } + ); + return data.policy || ""; +}; + +export const useGetSecretApprovalPolicyOfABoard = ({ + workspaceId, + secretPath = "/", + environment, + options = {} +}: TGetSecretApprovalPolicyOfBoardDTO & { + options?: UseQueryOptions< + TSecretApprovalPolicy, + unknown, + TSecretApprovalPolicy, + ReturnType + >; +}) => + useQuery({ + queryKey: secretApprovalKeys.getApprovalPolicyOfABoard(workspaceId, environment, secretPath), + queryFn: () => fetchApprovalPolicyOfABoard(workspaceId, environment, secretPath), + ...options, + enabled: Boolean(workspaceId && secretPath && environment) && (options?.enabled ?? true) + }); diff --git a/frontend/src/hooks/api/secretApproval/types.ts b/frontend/src/hooks/api/secretApproval/types.ts index 8eb486664..0095d699c 100644 --- a/frontend/src/hooks/api/secretApproval/types.ts +++ b/frontend/src/hooks/api/secretApproval/types.ts @@ -1,14 +1,26 @@ export type TSecretApprovalPolicy = { _id: string; workspace: string; + name: string; environment: string; secretPath?: string; approvers: string[]; approvals: number; }; +export type TGetSecretApprovalPoliciesDTO = { + workspaceId: string; +}; + +export type TGetSecretApprovalPolicyOfBoardDTO = { + workspaceId: string; + environment: string; + secretPath: string; +}; + export type TCreateSecretPolicyDTO = { workspaceId: string; + name?: string; environment: string; secretPath?: string | null; approvers?: string[]; @@ -17,6 +29,7 @@ export type TCreateSecretPolicyDTO = { export type TUpdateSecretPolicyDTO = { id: string; + name?: string; approvers?: string[]; secretPath?: string | null; approvals?: number; diff --git a/frontend/src/hooks/api/secretApprovalRequest/index.tsx b/frontend/src/hooks/api/secretApprovalRequest/index.tsx new file mode 100644 index 000000000..a72399901 --- /dev/null +++ b/frontend/src/hooks/api/secretApprovalRequest/index.tsx @@ -0,0 +1,10 @@ +export { + usePerformSecretApprovalRequestMerge, + useUpdateSecretApprovalRequestStatus, + useUpdateSecretApprovalReviewStatus +} from "./mutation"; +export { + useGetSecretApprovalRequestCount, + useGetSecretApprovalRequestDetails, + useGetSecretApprovalRequests +} from "./queries"; diff --git a/frontend/src/hooks/api/secretApprovalRequest/mutation.tsx b/frontend/src/hooks/api/secretApprovalRequest/mutation.tsx new file mode 100644 index 000000000..94358ed79 --- /dev/null +++ b/frontend/src/hooks/api/secretApprovalRequest/mutation.tsx @@ -0,0 +1,59 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { secretApprovalRequestKeys } from "./queries"; +import { + TPerformSecretApprovalRequestMerge, + TUpdateSecretApprovalRequestStatusDTO, + TUpdateSecretApprovalReviewStatusDTO +} from "./types"; + +export const useUpdateSecretApprovalReviewStatus = () => { + const queryClient = useQueryClient(); + + return useMutation<{}, {}, TUpdateSecretApprovalReviewStatusDTO>({ + mutationFn: async ({ id, status }) => { + const { data } = await apiRequest.post(`/api/v1/secret-approval-requests/${id}/review`, { + status + }); + return data; + }, + onSuccess: (_, { id }) => { + queryClient.invalidateQueries(secretApprovalRequestKeys.detail({ id })); + } + }); +}; + +export const useUpdateSecretApprovalRequestStatus = () => { + const queryClient = useQueryClient(); + + return useMutation<{}, {}, TUpdateSecretApprovalRequestStatusDTO>({ + mutationFn: async ({ id, status }) => { + const { data } = await apiRequest.post(`/api/v1/secret-approval-requests/${id}/status`, { + status + }); + return data; + }, + onSuccess: (_, { id, workspaceId }) => { + queryClient.invalidateQueries(secretApprovalRequestKeys.detail({ id })); + queryClient.invalidateQueries(secretApprovalRequestKeys.count({ workspaceId })); + } + }); +}; + +export const usePerformSecretApprovalRequestMerge = () => { + const queryClient = useQueryClient(); + + return useMutation<{}, {}, TPerformSecretApprovalRequestMerge>({ + mutationFn: async ({ id }) => { + const { data } = await apiRequest.post(`/api/v1/secret-approval-requests/${id}/merge`); + return data; + }, + onSuccess: (_, { id, workspaceId }) => { + queryClient.invalidateQueries(secretApprovalRequestKeys.detail({ id })); + queryClient.invalidateQueries(secretApprovalRequestKeys.list({ workspaceId })); + queryClient.invalidateQueries(secretApprovalRequestKeys.count({ workspaceId })); + } + }); +}; diff --git a/frontend/src/hooks/api/secretApprovalRequest/queries.tsx b/frontend/src/hooks/api/secretApprovalRequest/queries.tsx new file mode 100644 index 000000000..db4f6f94c --- /dev/null +++ b/frontend/src/hooks/api/secretApprovalRequest/queries.tsx @@ -0,0 +1,224 @@ +import { + useInfiniteQuery, + UseInfiniteQueryOptions, + useQuery, + UseQueryOptions +} from "@tanstack/react-query"; + +import { + decryptAssymmetric, + decryptSymmetric +} from "@app/components/utilities/cryptography/crypto"; +import { apiRequest } from "@app/config/request"; + +import { UserWsKeyPair } from "../keys/types"; +import { decryptSecrets } from "../secrets/queries"; +import { DecryptedSecret } from "../secrets/types"; +import { + TGetSecretApprovalRequestCount, + TGetSecretApprovalRequestDetails, + TGetSecretApprovalRequestList, + TSecretApprovalRequest, + TSecretApprovalRequestCount, + TSecretApprovalSecChange, + TSecretApprovalSecChangeData +} from "./types"; + +export const secretApprovalRequestKeys = { + list: ({ + workspaceId, + environment, + status, + committer, + offset, + limit + }: TGetSecretApprovalRequestList) => + [ + { workspaceId, environment, status, committer, offset, limit }, + "secret-approval-requests" + ] as const, + detail: ({ id }: Omit) => + [{ id }, "secret-approval-request-detail"] as const, + count: ({ workspaceId }: TGetSecretApprovalRequestCount) => [ + { workspaceId }, + "secret-approval-request-count" + ] +}; + +export const decryptSecretApprovalSecret = ( + encSecret: TSecretApprovalSecChangeData, + decryptFileKey: UserWsKeyPair +) => { + const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string; + const key = decryptAssymmetric({ + ciphertext: decryptFileKey.encryptedKey, + nonce: decryptFileKey.nonce, + publicKey: decryptFileKey.sender.publicKey, + privateKey: PRIVATE_KEY + }); + + const secretKey = decryptSymmetric({ + ciphertext: encSecret.secretKeyCiphertext, + iv: encSecret.secretKeyIV, + tag: encSecret.secretKeyTag, + key + }); + + const secretValue = decryptSymmetric({ + ciphertext: encSecret.secretValueCiphertext, + iv: encSecret.secretValueIV, + tag: encSecret.secretValueTag, + key + }); + + const secretComment = decryptSymmetric({ + ciphertext: encSecret.secretCommentCiphertext, + iv: encSecret.secretCommentIV, + tag: encSecret.secretCommentTag, + key + }); + return { + _id: encSecret._id, + version: encSecret.version, + secretKey, + secretValue, + secretComment, + tags: encSecret.tags + }; +}; + +const fetchSecretApprovalRequestList = async ({ + workspaceId, + environment, + committer, + status = "open", + limit = 20, + offset +}: TGetSecretApprovalRequestList) => { + const { data } = await apiRequest.get<{ approvals: TSecretApprovalRequest[] }>( + "/api/v1/secret-approval-requests", + { + params: { + workspaceId, + environment, + committer, + status, + limit, + offset + } + } + ); + + return data.approvals; +}; + +export const useGetSecretApprovalRequests = ({ + workspaceId, + environment, + options = {}, + status, + limit = 20, + committer +}: TGetSecretApprovalRequestList & { + options?: Omit< + UseInfiniteQueryOptions< + TSecretApprovalRequest[], + unknown, + TSecretApprovalRequest[], + ReturnType + >, + "queryKey" | "queryFn" + >; +}) => + useInfiniteQuery({ + queryKey: secretApprovalRequestKeys.list({ + workspaceId, + environment, + committer, + status + }), + queryFn: ({ pageParam }) => + fetchSecretApprovalRequestList({ + workspaceId, + environment, + status, + committer, + limit, + offset: pageParam + }), + enabled: Boolean(workspaceId) && (options?.enabled ?? true), + getNextPageParam: (lastPage, pages) => { + if (lastPage.length && lastPage.length < limit) return undefined; + + return lastPage?.length !== 0 ? pages.length * limit : undefined; + } + }); + +const fetchSecretApprovalRequestDetails = async ({ + id +}: Omit) => { + const { data } = await apiRequest.get<{ approval: TSecretApprovalRequest }>( + `/api/v1/secret-approval-requests/${id}` + ); + + return data.approval; +}; + +export const useGetSecretApprovalRequestDetails = ({ + id, + decryptKey, + options = {} +}: TGetSecretApprovalRequestDetails & { + options?: Omit< + UseQueryOptions< + TSecretApprovalRequest, + unknown, + TSecretApprovalRequest, + ReturnType + >, + "queryKey" | "queryFn" + >; +}) => + useQuery({ + queryKey: secretApprovalRequestKeys.detail({ id }), + queryFn: () => fetchSecretApprovalRequestDetails({ id }), + select: (data) => ({ + ...data, + commits: data.commits.map(({ secretVersion, op, newVersion, secret }) => ({ + op, + secret, + secretVersion: secretVersion ? decryptSecrets([secretVersion], decryptKey)[0] : undefined, + newVersion: newVersion ? decryptSecretApprovalSecret(newVersion, decryptKey) : undefined + })) + }), + enabled: Boolean(id && decryptKey) && (options?.enabled ?? true) + }); + +const fetchSecretApprovalRequestCount = async ({ workspaceId }: TGetSecretApprovalRequestCount) => { + const { data } = await apiRequest.get<{ approvals: TSecretApprovalRequestCount }>( + "/api/v1/secret-approval-requests/count", + { params: { workspaceId } } + ); + + return data.approvals; +}; + +export const useGetSecretApprovalRequestCount = ({ + workspaceId, + options = {} +}: TGetSecretApprovalRequestCount & { + options?: Omit< + UseQueryOptions< + TSecretApprovalRequestCount, + unknown, + TSecretApprovalRequestCount, + ReturnType + >, + "queryKey" | "queryFn" + >; +}) => + useQuery({ + queryKey: secretApprovalRequestKeys.count({ workspaceId }), + queryFn: () => fetchSecretApprovalRequestCount({ workspaceId }), + enabled: Boolean(workspaceId) && (options?.enabled ?? true) + }); diff --git a/frontend/src/hooks/api/secretApprovalRequest/types.ts b/frontend/src/hooks/api/secretApprovalRequest/types.ts new file mode 100644 index 000000000..1e129a61c --- /dev/null +++ b/frontend/src/hooks/api/secretApprovalRequest/types.ts @@ -0,0 +1,113 @@ +import { UserWsKeyPair } from "../keys/types"; +import { TSecretApprovalPolicy } from "../secretApproval/types"; +import { EncryptedSecret } from "../secrets/types"; +import { WsTag } from "../tags/types"; + +export enum ApprovalStatus { + PENDING = "pending", + APPROVED = "approved", + REJECTED = "rejected" +} + +export enum CommitType { + DELETE = "delete", + UPDATE = "update", + CREATE = "create" +} + +export type TSecretApprovalSecChangeData = { + _id: string; + secretKeyCiphertext: string; + secretKeyIV: string; + secretKeyTag: string; + secretValueCiphertext: string; + secretValueIV: string; + secretValueTag: string; + secretCommentIV: string; + secretCommentTag: string; + secretCommentCiphertext: string; + skipMultilineEncoding?: boolean; + algorithm: "aes-256-gcm"; + keyEncoding: "utf8" | "base64"; + tags?: WsTag[]; + version: number; +}; + +export type TSecretApprovalSecChange = { + _id: string; + version: number; + secretKey: string; + secretValue: string; + secretComment: string; + tags?: string[]; +}; + +export type TSecretApprovalRequest< + T extends unknown = TSecretApprovalSecChangeData, + J extends unknown = EncryptedSecret +> = { + _id: string; + slug: string; + createdAt: string; + committer: string; + reviewers: { + member: string; + status: ApprovalStatus; + }[]; + workspace: string; + environment: string; + folderId: string; + secretPath: string; + hasMerged: boolean; + status: "open" | "close"; + policy: TSecretApprovalPolicy; + statusChangeBy: string; + conflicts: Array<{ secretId: string; op: CommitType.UPDATE }>; + commits: { + // if there is no secret means it was creation + secret?: { version: number }; + secretVersion: J; + // if there is no new version its for Delete + newVersion?: T; + op: CommitType; + }[]; +}; + +export type TSecretApprovalRequestCount = { + open: number; + closed: number; +}; + +export type TGetSecretApprovalRequestList = { + workspaceId: string; + environment?: string; + status?: "open" | "close"; + committer?: string; + limit?: number; + offset?: number; +}; + +export type TGetSecretApprovalRequestCount = { + workspaceId: string; +}; + +export type TGetSecretApprovalRequestDetails = { + id: string; + decryptKey: UserWsKeyPair; +}; + +export type TUpdateSecretApprovalReviewStatusDTO = { + status: ApprovalStatus; + id: string; +}; + +export type TUpdateSecretApprovalRequestStatusDTO = { + status: "open" | "close"; + id: string; + workspaceId: string; +}; + +export type TPerformSecretApprovalRequestMerge = { + id: string; + workspaceId: string; +}; diff --git a/frontend/src/hooks/api/secrets/mutations.tsx b/frontend/src/hooks/api/secrets/mutations.tsx index c1bf59c85..18c52c4cb 100644 --- a/frontend/src/hooks/api/secrets/mutations.tsx +++ b/frontend/src/hooks/api/secrets/mutations.tsx @@ -8,6 +8,7 @@ import { } from "@app/components/utilities/cryptography/crypto"; import { apiRequest } from "@app/config/request"; +import { secretApprovalRequestKeys } from "../secretApprovalRequest/queries"; import { secretSnapshotKeys } from "../secretSnapshots/queries"; import { secretKeys } from "./queries"; import { @@ -114,6 +115,7 @@ export const useCreateSecretV3 = ({ queryClient.invalidateQueries( secretSnapshotKeys.count({ environment, workspaceId, directory: secretPath }) ); + queryClient.invalidateQueries(secretApprovalRequestKeys.count({ workspaceId })); }, ...options }); @@ -173,6 +175,7 @@ export const useUpdateSecretV3 = ({ queryClient.invalidateQueries( secretSnapshotKeys.count({ environment, workspaceId, directory: secretPath }) ); + queryClient.invalidateQueries(secretApprovalRequestKeys.count({ workspaceId })); }, ...options }); @@ -209,6 +212,7 @@ export const useDeleteSecretV3 = ({ queryClient.invalidateQueries( secretSnapshotKeys.count({ environment, workspaceId, directory: secretPath }) ); + queryClient.invalidateQueries(secretApprovalRequestKeys.count({ workspaceId })); }, ...options }); @@ -261,6 +265,7 @@ export const useCreateSecretBatch = ({ queryClient.invalidateQueries( secretSnapshotKeys.count({ environment, workspaceId, directory: secretPath }) ); + queryClient.invalidateQueries(secretApprovalRequestKeys.count({ workspaceId })); }, ...options }); @@ -313,6 +318,7 @@ export const useUpdateSecretBatch = ({ queryClient.invalidateQueries( secretSnapshotKeys.count({ environment, workspaceId, directory: secretPath }) ); + queryClient.invalidateQueries(secretApprovalRequestKeys.count({ workspaceId })); }, ...options }); @@ -349,6 +355,7 @@ export const useDeleteSecretBatch = ({ queryClient.invalidateQueries( secretSnapshotKeys.count({ environment, workspaceId, directory: secretPath }) ); + queryClient.invalidateQueries(secretApprovalRequestKeys.count({ workspaceId })); }, ...options }); diff --git a/frontend/src/hooks/api/secrets/queries.tsx b/frontend/src/hooks/api/secrets/queries.tsx index a9cbc3b29..db000f5c8 100644 --- a/frontend/src/hooks/api/secrets/queries.tsx +++ b/frontend/src/hooks/api/secrets/queries.tsx @@ -26,7 +26,10 @@ export const secretKeys = { getSecretVersion: (secretId: string) => [{ secretId }, "secret-versions"] as const }; -const decryptSecrets = (encryptedSecrets: EncryptedSecret[], decryptFileKey: UserWsKeyPair) => { +export const decryptSecrets = ( + encryptedSecrets: EncryptedSecret[], + decryptFileKey: UserWsKeyPair +) => { const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string; const key = decryptAssymmetric({ ciphertext: decryptFileKey.encryptedKey, diff --git a/frontend/src/hooks/api/subscriptions/types.ts b/frontend/src/hooks/api/subscriptions/types.ts index 3fcdbb339..c61967eec 100644 --- a/frontend/src/hooks/api/subscriptions/types.ts +++ b/frontend/src/hooks/api/subscriptions/types.ts @@ -11,12 +11,21 @@ export type SubscriptionPlan = { rbac: boolean; secretVersioning: boolean; slug: string; + secretApproval: string; tier: number; workspaceLimit: number; workspacesUsed: number; environmentLimit: number; samlSSO: boolean; - status: "incomplete" | "incomplete_expired" | "trialing" | "active" | "past_due" | "canceled" | "unpaid" | null; + status: + | "incomplete" + | "incomplete_expired" + | "trialing" + | "active" + | "past_due" + | "canceled" + | "unpaid" + | null; trial_end: number | null; has_used_trial: boolean; }; diff --git a/frontend/src/hooks/api/types.ts b/frontend/src/hooks/api/types.ts index b46b5bfab..f6131e5b2 100644 --- a/frontend/src/hooks/api/types.ts +++ b/frontend/src/hooks/api/types.ts @@ -5,13 +5,19 @@ export type { TCloudIntegration, TIntegration } from "./integrations/types"; export type { UserWsKeyPair } from "./keys/types"; export type { Organization } from "./organization/types"; export type { TSecretApprovalPolicy } from "./secretApproval/types"; +export type { + TGetSecretApprovalRequestDetails, + TSecretApprovalRequest, + TSecretApprovalSecChange +} from "./secretApprovalRequest/types"; +export { ApprovalStatus, CommitType } from "./secretApprovalRequest/types"; export type { TSecretFolder } from "./secretFolders/types"; export type { TImportedSecrets, TSecretImports } from "./secretImports/types"; export * from "./secrets/types"; export type { CreateServiceTokenDTO, ServiceToken } from "./serviceTokens/types"; export type { SubscriptionPlan } from "./subscriptions/types"; export type { WsTag } from "./tags/types"; -export type { AddUserToWsDTO, AddUserToWsRes, OrgUser, User } from "./users/types"; +export type { AddUserToWsDTO, AddUserToWsRes, OrgUser, TWorkspaceUser, User } from "./users/types"; export type { TWebhook } from "./webhooks/types"; export type { CreateEnvironmentDTO, diff --git a/frontend/src/layouts/AppLayout/AppLayout.tsx b/frontend/src/layouts/AppLayout/AppLayout.tsx index e9a66ead2..8c1391687 100644 --- a/frontend/src/layouts/AppLayout/AppLayout.tsx +++ b/frontend/src/layouts/AppLayout/AppLayout.tsx @@ -66,6 +66,7 @@ import { useAddUserToWs, useCreateWorkspace, useGetOrgTrialUrl, + useGetSecretApprovalRequestCount, useLogoutUser, useUploadWsKey } from "@app/hooks/api"; @@ -114,7 +115,9 @@ export const AppLayout = ({ children }: LayoutProps) => { const { orgs, currentOrg } = useOrganization(); const { user } = useUser(); const { subscription } = useSubscription(); + const workspaceId = currentWorkspace?._id || ""; // const [ isLearningNoteOpen, setIsLearningNoteOpen ] = useState(true); + const { data: secretApprovalReqCount } = useGetSecretApprovalRequestCount({ workspaceId }); const isAddingProjectsAllowed = subscription?.workspaceLimit ? subscription.workspacesUsed < subscription.workspaceLimit @@ -477,21 +480,24 @@ export const AppLayout = ({ children }: LayoutProps) => { - {process.env.NEXT_PUBLIC_SECRET_APPROVAL === "true" && ( - - - - Admin Panel - - - - )} - {/* + + + + Secret approval + {Boolean(secretApprovalReqCount?.open) && ( + + {secretApprovalReqCount?.open} + + )} + + + + { IP Allowlist - */} + { )} - {infisicalPlatformVersion && ( + {infisicalPlatformVersion && (
- Platform Version: {infisicalPlatformVersion} + Platform Version: {infisicalPlatformVersion}
- )} + )} diff --git a/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/ProjectRoleModifySection.tsx b/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/ProjectRoleModifySection.tsx index 0dbbfa85e..dbfd6ec03 100644 --- a/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/ProjectRoleModifySection.tsx +++ b/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/ProjectRoleModifySection.tsx @@ -9,6 +9,7 @@ import { faLock, faNetworkWired, faPuzzlePiece, + faShield, faTags, faUser, faUsers @@ -18,7 +19,7 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import { Button, FormControl, Input, UpgradePlanModal } from "@app/components/v2"; -import { useOrganization, useSubscription, useWorkspace } from "@app/context"; +import { ProjectPermissionSub, useOrganization, useSubscription, useWorkspace } from "@app/context"; import { usePopUp } from "@app/hooks"; import { useCreateRole, useUpdateRole } from "@app/hooks/api"; import { TRole } from "@app/hooks/api/roles/types"; @@ -41,6 +42,12 @@ const SINGLE_PERMISSION_LIST = [ icon: faPuzzlePiece, formName: "integrations" }, + { + title: "Secret Protect policy", + subtitle: "Manage policies for secret protection for unauthorized secret changes", + icon: faShield, + formName: ProjectPermissionSub.SecretApproval + }, { title: "Roles", subtitle: "Role management control", diff --git a/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/ProjectRoleModifySection.utils.ts b/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/ProjectRoleModifySection.utils.ts index f81a0f295..fec02934c 100644 --- a/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/ProjectRoleModifySection.utils.ts +++ b/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/ProjectRoleModifySection.utils.ts @@ -1,6 +1,7 @@ /* eslint-disable no-param-reassign */ import { z } from "zod"; +import { ProjectPermissionSub } from "@app/context"; import { TProjectPermission } from "@app/hooks/api/roles/types"; const generalPermissionSchema = z @@ -41,6 +42,8 @@ export const formSchema = z.object({ tags: generalPermissionSchema, "audit-logs": generalPermissionSchema, "ip-allowlist": generalPermissionSchema, + // akhilmhdh: refactor all keys like below + [ProjectPermissionSub.SecretApproval]: generalPermissionSchema, workspace: z .object({ edit: z.boolean().optional(), diff --git a/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/SingleProjectPermission.tsx b/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/SingleProjectPermission.tsx index 884d7632a..54a58576f 100644 --- a/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/SingleProjectPermission.tsx +++ b/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/SingleProjectPermission.tsx @@ -6,6 +6,7 @@ import { motion } from "framer-motion"; import { twMerge } from "tailwind-merge"; import { Checkbox, Select, SelectItem } from "@app/components/v2"; +import { ProjectPermissionSub } from "@app/context"; import { useToggle } from "@app/hooks"; import { TFormSchema } from "./ProjectRoleModifySection.utils"; @@ -21,7 +22,8 @@ type Props = { | "environments" | "tags" | "audit-logs" - | "ip-allowlist"; + | "ip-allowlist" + | ProjectPermissionSub.SecretApproval; isNonEditable?: boolean; setValue: UseFormSetValue; control: Control; diff --git a/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx b/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx index 35243e75f..c6e229970 100644 --- a/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx +++ b/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx @@ -2,6 +2,7 @@ import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; import { useWorkspace } from "@app/context"; import { SecretApprovalPolicyList } from "./components/SecretApprovalPolicyList"; +import { SecretApprovalRequest } from "./components/SecretApprovalRequest"; enum TabSection { ApprovalRequests = "approval-requests", @@ -13,15 +14,18 @@ export const SecretApprovalPage = () => { const workspaceId = currentWorkspace?._id || ""; return ( -
+
-

Admin Panels

+

Secret Approvals

Secret PRs Policies + + + diff --git a/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/SecretApprovalPolicyList.tsx b/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/SecretApprovalPolicyList.tsx index 700b1a785..76633aa28 100644 --- a/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/SecretApprovalPolicyList.tsx +++ b/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/SecretApprovalPolicyList.tsx @@ -2,6 +2,7 @@ import { faFileShield, faPlus } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; +import { ProjectPermissionCan } from "@app/components/permissions"; import { Button, DeleteActionModal, @@ -13,8 +14,15 @@ import { Td, Th, THead, - Tr + Tr, + UpgradePlanModal } from "@app/components/v2"; +import { + ProjectPermissionActions, + ProjectPermissionSub, + useProjectPermission, + useSubscription +} from "@app/context"; import { usePopUp } from "@app/hooks"; import { useDeleteSecretApprovalPolicy, @@ -33,13 +41,19 @@ type Props = { export const SecretApprovalPolicyList = ({ workspaceId }: Props) => { const { handlePopUpToggle, handlePopUpOpen, handlePopUpClose, popUp } = usePopUp([ "secretPolicyForm", - "deletePolicy" + "deletePolicy", + "upgradePlan" ] as const); + const permission = useProjectPermission(); + const { subscription } = useSubscription(); const { createNotification } = useNotificationContext(); const { data: members } = useGetWorkspaceUsers(workspaceId); const { data: policies, isLoading: isPoliciesLoading } = useGetSecretApprovalPolicies({ - workspaceId + workspaceId, + options: { + enabled: permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval) + } }); const { mutateAsync: deleteSecretApprovalPolicy } = useDeleteSecretApprovalPolicy(); @@ -75,18 +89,33 @@ export const SecretApprovalPolicyList = ({ workspaceId }: Props) => {
- + {(isAllowed) => ( + + )} +
+ @@ -99,9 +128,11 @@ export const SecretApprovalPolicyList = ({ workspaceId }: Props) => { )} {!isPoliciesLoading && !policies?.length && ( - + + + )} {policies?.map((policy) => ( { onChange={(isOpen) => handlePopUpToggle("deletePolicy", isOpen)} onDeleteApproved={handleDeletePolicy} /> + handlePopUpToggle("upgradePlan", isOpen)} + text="You can add secret approval policy if you switch to Infisical's Team plan." + /> ); }; diff --git a/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/components/SecretApprovalPolicyRow.tsx b/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/components/SecretApprovalPolicyRow.tsx index c39ba2b69..2ed186272 100644 --- a/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/components/SecretApprovalPolicyRow.tsx +++ b/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/components/SecretApprovalPolicyRow.tsx @@ -2,6 +2,7 @@ import { useState } from "react"; import { faCheckCircle, faPencil, faTrash } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { ProjectPermissionCan } from "@app/components/permissions"; import { DropdownMenu, DropdownMenuContent, @@ -11,9 +12,9 @@ import { IconButton, Input, Td, - Tooltip, Tr } from "@app/components/v2"; +import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context"; import { useUpdateSecretApprovalPolicy } from "@app/hooks/api"; import { TSecretApprovalPolicy } from "@app/hooks/api/types"; import { TWorkspaceUser } from "@app/hooks/api/users/types"; @@ -35,9 +36,11 @@ export const SecretApprovalPolicyRow = ({ }: Props) => { const [selectedApprovers, setSelectedApprovers] = useState([]); const { mutate: updateSecretApprovalPolicy, isLoading } = useUpdateSecretApprovalPolicy(); + const permission = useProjectPermission(); return ( + diff --git a/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/components/SecretPolicyForm.tsx b/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/components/SecretPolicyForm.tsx index 0b8048c53..35ce44aee 100644 --- a/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/components/SecretPolicyForm.tsx +++ b/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/components/SecretPolicyForm.tsx @@ -33,12 +33,18 @@ type Props = { editValues?: TSecretApprovalPolicy; }; -const formSchema = z.object({ - environment: z.string(), - secretPath: z.string().optional().nullable(), - approvals: z.number().min(1), - approvers: z.string().array().optional() -}); +const formSchema = z + .object({ + environment: z.string(), + name: z.string().optional(), + secretPath: z.string().optional().nullable(), + approvals: z.number().min(1), + approvers: z.string().array().min(1) + }) + .refine((data) => data.approvals <= data.approvers.length, { + path: ["approvals"], + message: "The number of approvals should be lower than the number of approvers." + }); type TFormSchema = z.infer; @@ -126,6 +132,15 @@ export const SecretPolicyForm = ({
+ ( + + + + )} + /> ( @@ -184,7 +199,7 @@ export const SecretPolicyForm = ({ style={{ width: "var(--radix-dropdown-menu-trigger-width)" }} align="start" > - Select members that must approve changes + Select members that are allowed to approve changes {members.map(({ _id, user }) => { const isChecked = value?.includes(_id); return ( diff --git a/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/SecretApprovalRequest.tsx b/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/SecretApprovalRequest.tsx new file mode 100644 index 000000000..959c714b4 --- /dev/null +++ b/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/SecretApprovalRequest.tsx @@ -0,0 +1,273 @@ +import { Fragment, useState } from "react"; +import { + faCheck, + faCheckCircle, + faChevronDown, + faCodeBranch +} from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { formatDistance } from "date-fns"; +import { AnimatePresence, motion } from "framer-motion"; + +import { + Button, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuTrigger, + EmptyState, + Skeleton +} from "@app/components/v2"; +import { useUser, useWorkspace } from "@app/context"; +import { + useGetSecretApprovalRequestCount, + useGetSecretApprovalRequests, + useGetWorkspaceUsers +} from "@app/hooks/api"; +import { ApprovalStatus, TSecretApprovalRequest, TWorkspaceUser } from "@app/hooks/api/types"; + +import { + generateCommitText, + SecretApprovalRequestChanges +} from "./components/SecretApprovalRequestChanges"; + +export const SecretApprovalRequest = () => { + const { currentWorkspace } = useWorkspace(); + const workspaceId = currentWorkspace?._id || ""; + const [selectedApproval, setSelectedApproval] = useState(null); + + // filters + const [statusFilter, setStatusFilter] = useState<"open" | "close">("open"); + const [envFilter, setEnvFilter] = useState(); + const [committerFilter, setCommitterFilter] = useState(); + + const { + data: secretApprovalRequests, + isFetchingNextPage: isFetchingNextApprovalRequest, + fetchNextPage: fetchNextApprovalRequest, + hasNextPage: hasNextApprovalPage, + isLoading: isApprovalRequestLoading, + refetch + } = useGetSecretApprovalRequests({ + workspaceId, + status: statusFilter, + environment: envFilter, + committer: committerFilter + }); + const { data: secretApprovalRequestCount, isSuccess: isSecretApprovalReqCountSuccess } = + useGetSecretApprovalRequestCount({ workspaceId }); + const { user: presentUser } = useUser(); + const { data: members } = useGetWorkspaceUsers(workspaceId); + const membersGroupById = members?.reduce>( + (prev, curr) => ({ ...prev, [curr._id]: curr }), + {} + ); + const myMembershipId = members?.find(({ user }) => user._id === presentUser._id)?._id; + const isSecretApprovalScreen = Boolean(selectedApproval); + + const handleGoBackSecretRequestDetail = () => { + setSelectedApproval(null); + refetch({ refetchPage: (_page, index) => index === 0 }); + }; + + const isRequestListEmpty = + !isApprovalRequestLoading && secretApprovalRequests?.pages[0]?.length === 0; + + return ( + + {isSecretApprovalScreen ? ( + + + + ) : ( + +
+
setStatusFilter("open")} + onKeyDown={(evt) => { + if (evt.key === "Enter") setStatusFilter("open"); + }} + className={ + statusFilter === "close" ? "text-gray-500 hover:text-gray-400 duration-100" : "" + } + > + + {isSecretApprovalReqCountSuccess && secretApprovalRequestCount?.open} Open +
+
setStatusFilter("close")} + onKeyDown={(evt) => { + if (evt.key === "Enter") setStatusFilter("close"); + }} + > + + {isSecretApprovalReqCountSuccess && secretApprovalRequestCount.closed} Closed +
+
+ + + + + + Select an environment + {currentWorkspace?.environments.map(({ slug, name }) => ( + setEnvFilter((state) => (state === slug ? undefined : slug))} + key={`request-filter-${slug}`} + icon={envFilter === slug && } + iconPos="right" + > + {name} + + ))} + + + + + + + + Select an author + {members?.map(({ user, _id }) => ( + + setCommitterFilter((state) => (state === _id ? undefined : _id)) + } + key={`request-filter-member-${_id}`} + icon={committerFilter === _id && } + iconPos="right" + > + {user.email} + + ))} + + +
+
+
+ {isRequestListEmpty && ( +
+ +
+ )} + {secretApprovalRequests?.pages?.map((group, i) => ( + + {group?.map((secretApproval) => { + const { + _id: reqId, + commits, + committer, + createdAt, + policy, + reviewers, + status + } = secretApproval; + const isApprover = policy?.approvers?.indexOf(myMembershipId || "") !== -1; + const isReviewed = + reviewers.findIndex( + ({ member, status: reviewStatus }) => + member === myMembershipId && reviewStatus === ApprovalStatus.APPROVED + ) !== -1; + return ( +
setSelectedApproval(secretApproval)} + onKeyDown={(evt) => { + if (evt.key === "Enter") setSelectedApproval(secretApproval); + }} + > +
+ + {generateCommitText(commits)} + #{secretApproval.slug} +
+ + Opened {formatDistance(new Date(createdAt), new Date())} ago by{" "} + {membersGroupById?.[committer]?.user?.firstName}{" "} + {membersGroupById?.[committer]?.user?.lastName} ( + {membersGroupById?.[committer]?.user?.email}){" "} + {isApprover && !isReviewed && status === "open" && "- Review required"} + +
+ ); + })} +
+ ))} + {(isFetchingNextApprovalRequest || isApprovalRequestLoading) && ( +
+ {Array.apply(0, Array(3)).map((_x, index) => ( +
+
+ + +
+ +
+ ))} +
+ )} +
+ {hasNextApprovalPage && ( + + )} +
+ )} +
+ ); +}; diff --git a/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/components/SecretApprovalRequestAction.tsx b/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/components/SecretApprovalRequestAction.tsx new file mode 100644 index 000000000..73d72b4a5 --- /dev/null +++ b/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/components/SecretApprovalRequestAction.tsx @@ -0,0 +1,163 @@ +import { + faCheck, + faClose, + faLockOpen, + faSquareCheck, + faSquareXmark, + faUserLock +} from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { twMerge } from "tailwind-merge"; + +import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; +import { Button } from "@app/components/v2"; +import { + usePerformSecretApprovalRequestMerge, + useUpdateSecretApprovalRequestStatus +} from "@app/hooks/api"; + +type Props = { + approvalRequestId: string; + hasMerged?: boolean; + isMergable?: boolean; + status: "close" | "open"; + approvals: number; + statusChangeByEmail: string; + workspaceId: string; +}; + +export const SecretApprovalRequestAction = ({ + approvalRequestId, + hasMerged, + status, + isMergable, + approvals, + statusChangeByEmail, + workspaceId +}: Props) => { + const { createNotification } = useNotificationContext(); + const { mutateAsync: performSecretApprovalMerge, isLoading: isMerging } = + usePerformSecretApprovalRequestMerge(); + + const { mutateAsync: updateSecretStatusChange, isLoading: isStatusChanging } = + useUpdateSecretApprovalRequestStatus(); + + const handleSecretApprovalRequestMerge = async () => { + try { + await performSecretApprovalMerge({ + id: approvalRequestId, + workspaceId + }); + createNotification({ + type: "success", + text: "Successfully merged the request" + }); + } catch (err) { + console.log(err); + createNotification({ + type: "error", + text: "Failed to update the request status" + }); + } + }; + + const handleSecretApprovalStatusChange = async (reqState: "open" | "close") => { + try { + await updateSecretStatusChange({ + id: approvalRequestId, + status: reqState, + workspaceId + }); + createNotification({ + type: "success", + text: "Successfully updated the request" + }); + } catch (err) { + console.log(err); + createNotification({ + type: "error", + text: "Failed to update the request status" + }); + } + }; + + if (!hasMerged && status === "open") { + return ( +
+
+ + + {isMergable ? "Good to merge" : "Review required"} + + At least {approvals} approving review required + {Boolean(statusChangeByEmail) && `. Reopened by ${statusChangeByEmail}`} + + +
+
+ + +
+
+ ); + } + + if (hasMerged && status === "close") + return ( +
+
+ + + Change request merged + + Merged by {statusChangeByEmail} + + +
+
+ ); + + return ( +
+
+ + + Change request has been closed + + Closed by {statusChangeByEmail} + + +
+
+ +
+
+ ); +}; diff --git a/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/components/SecretApprovalRequestChangeItem.tsx b/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/components/SecretApprovalRequestChangeItem.tsx new file mode 100644 index 000000000..d921f9975 --- /dev/null +++ b/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/components/SecretApprovalRequestChangeItem.tsx @@ -0,0 +1,177 @@ +import { faExclamationTriangle, faInfo } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { + SecretInput, + Table, + TableContainer, + Tag, + TBody, + Td, + Th, + THead, + Tooltip, + Tr +} from "@app/components/v2"; +import { CommitType, DecryptedSecret, TSecretApprovalSecChange, WsTag } from "@app/hooks/api/types"; + +export type Props = { + op: CommitType; + secretVersion?: DecryptedSecret; + newVersion?: Omit & { tags?: WsTag[] }; + presentSecretVersionNumber: number; + hasMerged?: Boolean; + conflicts: Array<{ secretId: string; op: CommitType }>; +}; + +const generateItemTitle = (op: CommitType) => { + let text = { label: "", color: "" }; + if (op === CommitType.CREATE) text = { label: "create", color: "#16a34a" }; + else if (op === CommitType.UPDATE) text = { label: "change", color: "#ea580c" }; + else text = { label: "deletion", color: "#b91c1c" }; + + return ( + + Request for secret {text.label} + + ); +}; + +const generateConflictText = (op: CommitType) => { + if (op === CommitType.CREATE) return
Secret already exist
; + if (op === CommitType.UPDATE) return
Secret not found
; + return null; +}; + +export const SecretApprovalRequestChangeItem = ({ + op, + secretVersion, + newVersion, + presentSecretVersionNumber, + hasMerged, + conflicts +}: Props) => { + // meaning request has changed + const isStale = (secretVersion?.version || 1) < presentSecretVersionNumber; + const itemConflict = + hasMerged && conflicts.find((el) => el.op === op && el.secretId === newVersion?._id); + const hasConflict = Boolean(itemConflict); + + return ( +
+
+
{generateItemTitle(op)}
+ {!hasMerged && isStale && ( +
+ + Secret has been changed(stale) +
+ )} + {hasMerged && hasConflict && ( +
+ + + +
{generateConflictText(op)}
+
+ )} +
+ +
Name Environment Secret Path Eligible Approvers - -
+ +
{policy.name} {policy.environment} {policy.secretPath || "*"} @@ -61,7 +64,13 @@ export const SecretApprovalPolicyRow = ({ } }} > - + - Select members that must approve changes + Select members that are allowed to approve changes {members?.map(({ _id, user }) => { const isChecked = selectedApprovers.includes(_id); return ( @@ -97,22 +106,37 @@ export const SecretApprovalPolicyRow = ({ {policy.approvals}
- - - - - - - - - - + + {(isAllowed) => ( + + + + )} + + + {(isAllowed) => ( + + + + )} +
+ + + {op === CommitType.UPDATE && + + + + + + {op === CommitType.UPDATE ? ( + + + + + + + + + + + + + + + + + ) : ( + + + + + + + + + )} +
} + SecretValueCommentTags
OLD{secretVersion?.key} + + {secretVersion?.comment} + {secretVersion?.tags?.map(({ name, _id: tagId, tagColor }) => ( + +
+
{name}
+ + ))} +
NEW{newVersion?.secretKey} + + {newVersion?.secretComment} + {newVersion?.tags?.map(({ name, _id: tagId, tagColor }) => ( + +
+
{name}
+ + ))} +
{op === CommitType.CREATE ? newVersion?.secretKey : secretVersion?.key} + + + {op === CommitType.CREATE ? newVersion?.secretComment : secretVersion?.comment} + + {(op === CommitType.CREATE ? newVersion?.tags : secretVersion?.tags)?.map( + ({ name, _id: tagId, tagColor }) => ( + +
+
{name}
+ + ) + )} +
+
+ + ); +}; diff --git a/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/components/SecretApprovalRequestChanges.tsx b/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/components/SecretApprovalRequestChanges.tsx new file mode 100644 index 000000000..b90e156bf --- /dev/null +++ b/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/components/SecretApprovalRequestChanges.tsx @@ -0,0 +1,302 @@ +import { ReactNode } from "react"; +import { + faArrowLeft, + faCheck, + faCheckCircle, + faCircle, + faCodeBranch, + faFolder, + faXmarkCircle +} from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; +import { Button, ContentLoader, EmptyState, IconButton, Tooltip } from "@app/components/v2"; +import { useUser } from "@app/context"; +import { + useGetSecretApprovalRequestDetails, + useGetUserWsKey, + useUpdateSecretApprovalReviewStatus +} from "@app/hooks/api"; +import { ApprovalStatus, CommitType, TWorkspaceUser } from "@app/hooks/api/types"; + +import { SecretApprovalRequestAction } from "./SecretApprovalRequestAction"; +import { SecretApprovalRequestChangeItem } from "./SecretApprovalRequestChangeItem"; + +export const generateCommitText = (commits: { op: CommitType }[] = []) => { + const score: Record = {}; + commits.forEach(({ op }) => { + score[op] = (score?.[op] || 0) + 1; + }); + const text: ReactNode[] = []; + if (score[CommitType.CREATE]) + text.push( + + {score[CommitType.CREATE]} secret{score[CommitType.CREATE] !== 1 && "s"} + created + + ); + if (score[CommitType.UPDATE]) + text.push( + + {Boolean(text.length) && ","} + {score[CommitType.UPDATE]} secret{score[CommitType.UPDATE] !== 1 && "s"} + + {" "} + updated + + + ); + if (score[CommitType.DELETE]) + text.push( + + {Boolean(text.length) && "and"} + {score[CommitType.DELETE]} secret{score[CommitType.UPDATE] !== 1 && "s"} + deleted + + ); + + return text; +}; + +const getReviewedStatusSymbol = (status?: ApprovalStatus) => { + if (status === ApprovalStatus.APPROVED) + return ; + if (status === ApprovalStatus.REJECTED) + return ; + return ; +}; + +type Props = { + workspaceId: string; + approvalRequestId: string; + onGoBack: () => void; + committer?: TWorkspaceUser; + members?: Record; +}; + +export const SecretApprovalRequestChanges = ({ + approvalRequestId, + onGoBack, + committer, + workspaceId, + members = {} +}: Props) => { + const { createNotification } = useNotificationContext(); + const { user } = useUser(); + const { data: decryptFileKey } = useGetUserWsKey(workspaceId); + const { + data: secretApprovalRequestDetails, + isSuccess: isSecretApprovalRequestSuccess, + isLoading: isSecretApprovalRequestLoading + } = useGetSecretApprovalRequestDetails({ + id: approvalRequestId, + decryptKey: decryptFileKey! + }); + + const { + mutateAsync: updateSecretApprovalRequestStatus, + isLoading: isUpdatingRequestStatus, + variables + } = useUpdateSecretApprovalReviewStatus(); + + const isApproving = variables?.status === ApprovalStatus.APPROVED && isUpdatingRequestStatus; + const isRejecting = variables?.status === ApprovalStatus.REJECTED && isUpdatingRequestStatus; + + // membership of present user + const myMembership = Object.values(members).find( + ({ user: membershipUser }) => membershipUser.email === user.email + ); + const myMembershipId = myMembership?._id || ""; + const reviewedMembers = secretApprovalRequestDetails?.reviewers?.reduce< + Record + >( + (prev, curr) => ({ + ...prev, + [curr.member]: curr.status + }), + {} + ); + const hasApproved = reviewedMembers?.[myMembershipId] === ApprovalStatus.APPROVED; + const hasRejected = reviewedMembers?.[myMembershipId] === ApprovalStatus.REJECTED; + + const handleSecretApprovalStatusUpdate = async (status: ApprovalStatus) => { + try { + await updateSecretApprovalRequestStatus({ + id: approvalRequestId, + status + }); + createNotification({ + type: "success", + text: `Successfully ${status} the request` + }); + } catch (err) { + console.log(err); + createNotification({ + type: "error", + text: "Failed to update the request status" + }); + } + }; + + if (isSecretApprovalRequestLoading) { + return ( +
+ +
+ ); + } + + if (!isSecretApprovalRequestSuccess) + return ( +
+ +
+ ); + + const isMergable = + secretApprovalRequestDetails?.policy?.approvals <= + secretApprovalRequestDetails?.policy?.approvers?.filter( + (approverId) => reviewedMembers?.[approverId] === ApprovalStatus.APPROVED + ).length; + const hasMerged = secretApprovalRequestDetails?.hasMerged; + + return ( +
+
+
+ + + +
+ + {secretApprovalRequestDetails.status} +
+
+
+ {generateCommitText(secretApprovalRequestDetails.commits)} +
+
+ {committer?.user?.firstName} + {committer?.user?.lastName} ({committer?.user?.email}) wants to change{" "} + {secretApprovalRequestDetails.commits.length} secret values in + + {secretApprovalRequestDetails.environment} + +
+
+ +
+
{secretApprovalRequestDetails.secretPath}
+
+
+
+ {!hasMerged && secretApprovalRequestDetails.status === "open" && ( + <> + + + + )} +
+
+ {secretApprovalRequestDetails.commits.map( + ({ op, secretVersion, secret, newVersion }, index) => ( + + ) + )} +
+
+ +
+
+
+
Reviewers
+
+ {secretApprovalRequestDetails?.policy?.approvers.map((requiredApproverId) => { + const userDetails = members?.[requiredApproverId]?.user; + const status = reviewedMembers?.[requiredApproverId]; + return ( +
+
+ + {userDetails?.email} + + * +
+
+ + {getReviewedStatusSymbol(status)} + +
+
+ ); + })} + {secretApprovalRequestDetails?.reviewers + .filter( + ({ member }) => !secretApprovalRequestDetails?.policy?.approvers?.includes(member) + ) + .map((reviewer) => { + const userDetails = members?.[reviewer.member]?.user; + const status = reviewedMembers?.[reviewer.status]; + return ( +
+
+ + {userDetails?.email} + + * +
+
+ + {getReviewedStatusSymbol(status)} + +
+
+ ); + })} +
+
+
+ ); +}; diff --git a/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/index.tsx b/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/index.tsx new file mode 100644 index 000000000..5752309ca --- /dev/null +++ b/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/index.tsx @@ -0,0 +1 @@ +export { SecretApprovalRequest } from "./SecretApprovalRequest"; diff --git a/frontend/src/views/SecretMainPage/SecretMainPage.tsx b/frontend/src/views/SecretMainPage/SecretMainPage.tsx index 60fe31dce..4ffa27c86 100644 --- a/frontend/src/views/SecretMainPage/SecretMainPage.tsx +++ b/frontend/src/views/SecretMainPage/SecretMainPage.tsx @@ -19,6 +19,7 @@ import { useGetImportedSecrets, useGetProjectFolders, useGetProjectSecrets, + useGetSecretApprovalPolicyOfABoard, useGetSecretImports, useGetUserWsKey, useGetWorkspaceSnapshotList, @@ -118,6 +119,13 @@ export const SecretMainPage = () => { // fetch tags const { data: tags } = useGetWsTags(canReadSecret ? workspaceId : ""); + const { data: boardPolicy } = useGetSecretApprovalPolicyOfABoard({ + workspaceId, + environment, + secretPath + }); + const isProtectedBranch = Boolean(boardPolicy); + const { data: snapshotList, isFetchingNextPage: isFetchingNextSnapshotList, @@ -207,6 +215,8 @@ export const SecretMainPage = () => { secretPath={secretPath} isProjectRelated onEnvChange={handleEnvChange} + isProtectedBranch={isProtectedBranch} + protectionPolicyName={boardPolicy?.name} /> {!isRollbackMode ? ( @@ -281,6 +291,7 @@ export const SecretMainPage = () => { workspaceId={workspaceId} secretPath={secretPath} decryptFileKey={decryptFileKey!} + isProtectedBranch={isProtectedBranch} /> )} {!canReadSecret && folders?.length === 0 && } @@ -292,6 +303,7 @@ export const SecretMainPage = () => { decryptFileKey={decryptFileKey!} secretPath={secretPath} autoCapitalize={currentWorkspace?.autoCapitalization} + isProtectedBranch={isProtectedBranch} /> { secretPath={secretPath} isSmaller={isNotEmtpy} environments={currentWorkspace?.environments} + isProtectedBranch={isProtectedBranch} /> { const { register, @@ -62,7 +64,9 @@ export const CreateSecretForm = ({ reset(); createNotification({ type: "success", - text: "Successfully created secret" + text: isProtectedBranch + ? "Requested changes have been sent for review" + : "Successfully created secret" }); } catch (error) { console.log(error); diff --git a/frontend/src/views/SecretMainPage/components/SecretDropzone/SecretDropzone.tsx b/frontend/src/views/SecretMainPage/components/SecretDropzone/SecretDropzone.tsx index 838a5f0fb..3a33c2688 100644 --- a/frontend/src/views/SecretMainPage/components/SecretDropzone/SecretDropzone.tsx +++ b/frontend/src/views/SecretMainPage/components/SecretDropzone/SecretDropzone.tsx @@ -43,6 +43,7 @@ type Props = { environment: string; secretPath: string; secrets?: DecryptedSecret[]; + isProtectedBranch?: boolean; }; export const SecretDropzone = ({ @@ -52,7 +53,8 @@ export const SecretDropzone = ({ decryptFileKey, environment, secretPath, - secrets = [] + secrets = [], + isProtectedBranch = false }: Props): JSX.Element => { const { t } = useTranslation(); const [isDragActive, setDragActive] = useToggle(); @@ -195,7 +197,9 @@ export const SecretDropzone = ({ handlePopUpClose("overlapKeyWarning"); createNotification({ type: "success", - text: "Successfully uploaded secrets" + text: isProtectedBranch + ? "Uploaded changes have been sent for review" + : "Successfully uploaded secrets" }); } catch (err) { console.log(err); diff --git a/frontend/src/views/SecretMainPage/components/SecretListView/SecretDetaiSidebar.tsx b/frontend/src/views/SecretMainPage/components/SecretListView/SecretDetaiSidebar.tsx index a6bdd763c..0a9f38bc4 100644 --- a/frontend/src/views/SecretMainPage/components/SecretListView/SecretDetaiSidebar.tsx +++ b/frontend/src/views/SecretMainPage/components/SecretListView/SecretDetaiSidebar.tsx @@ -49,7 +49,8 @@ type Props = { onDeleteSecret: () => void; onSaveSecret: ( orgSec: DecryptedSecret, - modSec: Omit & { tags: { _id: string }[] } + modSec: Omit & { tags: { _id: string }[] }, + cb?: () => void ) => Promise; tags: WsTag[]; onCreateTag: () => void; @@ -143,7 +144,7 @@ export const SecretDetailSidebar = ({ }; const handleFormSubmit = async (data: TFormSchema) => { - await onSaveSecret(secret, { ...secret, ...data }); + await onSaveSecret(secret, { ...secret, ...data }, () => reset()); }; return ( diff --git a/frontend/src/views/SecretMainPage/components/SecretListView/SecretItem.tsx b/frontend/src/views/SecretMainPage/components/SecretListView/SecretItem.tsx index a2eb5eb31..c420cdd07 100644 --- a/frontend/src/views/SecretMainPage/components/SecretListView/SecretItem.tsx +++ b/frontend/src/views/SecretMainPage/components/SecretListView/SecretItem.tsx @@ -54,7 +54,8 @@ type Props = { secret: DecryptedSecret; onSaveSecret: ( orgSec: DecryptedSecret, - modSec: Omit & { tags: { _id: string }[] } + modSec: Omit & { tags: { _id: string }[] }, + cb?: () => void ) => Promise; onDeleteSecret: (sec: DecryptedSecret) => void; onDetailViewSecret: (sec: DecryptedSecret) => void; @@ -148,13 +149,14 @@ export const SecretItem = memo( ); setValue("valueOverride", secret?.valueOverride, { shouldDirty: !isUnsavedOverride }); } else { + reset(); setValue("overrideAction", SecretActionType.Modified, { shouldDirty: true }); setValue("valueOverride", "", { shouldDirty: true }); } }; const handleFormSubmit = async (data: TFormSchema) => { - await onSaveSecret(secret, { ...secret, ...data }); + await onSaveSecret(secret, { ...secret, ...data }, () => reset()); }; const handleTagSelect = (tag: WsTag) => { diff --git a/frontend/src/views/SecretMainPage/components/SecretListView/SecretListView.tsx b/frontend/src/views/SecretMainPage/components/SecretListView/SecretListView.tsx index 050480456..3863e55bf 100644 --- a/frontend/src/views/SecretMainPage/components/SecretListView/SecretListView.tsx +++ b/frontend/src/views/SecretMainPage/components/SecretListView/SecretListView.tsx @@ -27,6 +27,7 @@ type Props = { sortDir?: SortDir; tags?: WsTag[]; isVisible?: boolean; + isProtectedBranch?: boolean; }; const reorderSecretGroupByUnderscore = (secrets: DecryptedSecret[], sortDir: SortDir) => { @@ -84,7 +85,8 @@ export const SecretListView = ({ filter, sortDir = SortDir.ASC, tags: wsTags = [], - isVisible + isVisible, + isProtectedBranch = false }: Props) => { const { createNotification } = useNotificationContext(); const queryClient = useQueryClient(); @@ -178,7 +180,8 @@ export const SecretListView = ({ const handleSaveSecret = useCallback( async ( orgSecret: DecryptedSecret, - modSecret: Omit & { tags: { _id: string }[] } + modSecret: Omit & { tags: { _id: string }[] }, + cb?: () => void ) => { const { key: oldKey } = orgSecret; const { key, value, overrideAction, idOverride, valueOverride, tags, comment } = modSecret; @@ -193,6 +196,19 @@ export const SecretListView = ({ ) && isSameTags; try { + // personal secret change + if (overrideAction === "deleted") { + await handleSecretOperation("delete", "personal", oldKey); + } else if (overrideAction && idOverride) { + await handleSecretOperation("update", "personal", oldKey, { + value: valueOverride, + newKey: hasKeyChanged ? key : undefined, + skipMultilineEncoding: modSecret.skipMultilineEncoding + }); + } else if (overrideAction) { + await handleSecretOperation("create", "personal", oldKey, { value: valueOverride }); + } + // shared secret change if (!isSharedSecUnchanged) { await handleSecretOperation("update", "shared", oldKey, { @@ -202,19 +218,7 @@ export const SecretListView = ({ newKey: hasKeyChanged ? key : undefined, skipMultilineEncoding: modSecret.skipMultilineEncoding }); - } - - // personal secret change - if (overrideAction === "deleted") { - await handleSecretOperation("delete", "personal", key); - } else if (overrideAction && idOverride) { - await handleSecretOperation("update", "personal", oldKey, { - value: valueOverride, - newKey: hasKeyChanged ? key : undefined, - skipMultilineEncoding: modSecret.skipMultilineEncoding - }); - } else if (overrideAction) { - await handleSecretOperation("create", "personal", key, { value: valueOverride }); + if (cb) cb(); } queryClient.invalidateQueries( @@ -229,7 +233,9 @@ export const SecretListView = ({ handlePopUpClose("secretDetail"); createNotification({ type: "success", - text: "Successfully saved secrets" + text: isProtectedBranch + ? "Requested changes have been sent for review" + : "Successfully saved secrets" }); } catch (error) { console.log(error); @@ -239,7 +245,7 @@ export const SecretListView = ({ }); } }, - [environment, secretPath] + [environment, secretPath, isProtectedBranch] ); const handleSecretDelete = useCallback(async () => { @@ -259,7 +265,9 @@ export const SecretListView = ({ handlePopUpClose("secretDetail"); createNotification({ type: "success", - text: "Successfully deleted secret" + text: isProtectedBranch + ? "Requested changes have been sent for review" + : "Successfully deleted secret" }); } catch (error) { console.log(error);