Merge branch 'Infisical:main' into cloudflare-pages-integration

This commit is contained in:
Stijn-Kuijper
2023-06-18 18:26:55 +02:00
committed by GitHub
94 changed files with 2538 additions and 2373 deletions

4
.github/values.yaml vendored
View File

@@ -6,7 +6,7 @@ frontend:
secrets.infisical.com/auto-reload: "true" secrets.infisical.com/auto-reload: "true"
replicaCount: 2 replicaCount: 2
image: image:
repository: infisical/frontend repository: infisical/staging_deployment_frontend
tag: "latest" tag: "latest"
pullPolicy: Always pullPolicy: Always
kubeSecretRef: managed-secret-frontend kubeSecretRef: managed-secret-frontend
@@ -25,7 +25,7 @@ backend:
secrets.infisical.com/auto-reload: "true" secrets.infisical.com/auto-reload: "true"
replicaCount: 2 replicaCount: 2
image: image:
repository: infisical/backend repository: infisical/staging_deployment_backend
tag: "latest" tag: "latest"
pullPolicy: Always pullPolicy: Always
kubeSecretRef: managed-backend-secret kubeSecretRef: managed-backend-secret

View File

@@ -0,0 +1,118 @@
name: Release production images (frontend, backend)
on:
push:
tags:
- "infisical/v*.*.*"
jobs:
backend-image:
name: Build backend image
runs-on: ubuntu-latest
steps:
- name: Extract version from tag
id: extract_version
run: echo "::set-output name=version::${GITHUB_REF_NAME#infisical/}"
- name: ☁️ Checkout source
uses: actions/checkout@v3
- name: 📦 Install dependencies to test all dependencies
run: npm ci --only-production
working-directory: backend
- name: 🧪 Run tests
run: npm run test:ci
working-directory: backend
- name: Save commit hashes for tag
id: commit
uses: pr-mpt/actions-commit-hash@v2
- name: 🔧 Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: 🐋 Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up Depot CLI
uses: depot/setup-action@v1
- name: 📦 Build backend and export to Docker
uses: depot/build-push-action@v1
with:
project: 64mmf0n610
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
load: true
context: backend
tags: infisical/backend:test
- name: ⏻ Spawn backend container and dependencies
run: |
docker compose -f .github/resources/docker-compose.be-test.yml up --wait --quiet-pull
- name: 🧪 Test backend image
run: |
./.github/resources/healthcheck.sh infisical-backend-test
- name: ⏻ Shut down backend container and dependencies
run: |
docker compose -f .github/resources/docker-compose.be-test.yml down
- name: 🏗️ Build backend and push
uses: depot/build-push-action@v1
with:
project: 64mmf0n610
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
push: true
context: backend
tags: |
infisical/backend:${{ steps.commit.outputs.short }}
infisical/backend:latest
infisical/backend:${{ steps.extract_version.outputs.version }}
platforms: linux/amd64,linux/arm64
frontend-image:
name: Build frontend image
runs-on: ubuntu-latest
steps:
- name: Extract version from tag
id: extract_version
run: echo "::set-output name=version::${GITHUB_REF_NAME#infisical/}"
- name: ☁️ Checkout source
uses: actions/checkout@v3
- name: Save commit hashes for tag
id: commit
uses: pr-mpt/actions-commit-hash@v2
- name: 🔧 Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: 🐋 Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up Depot CLI
uses: depot/setup-action@v1
- name: 📦 Build frontend and export to Docker
uses: depot/build-push-action@v1
with:
load: true
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
project: 64mmf0n610
context: frontend
tags: infisical/frontend:test
build-args: |
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
- name: ⏻ Spawn frontend container
run: |
docker run -d --rm --name infisical-frontend-test infisical/frontend:test
- name: 🧪 Test frontend image
run: |
./.github/resources/healthcheck.sh infisical-frontend-test
- name: ⏻ Shut down frontend container
run: |
docker stop infisical-frontend-test
- name: 🏗️ Build frontend and push
uses: depot/build-push-action@v1
with:
project: 64mmf0n610
push: true
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
context: frontend
tags: |
infisical/frontend:${{ steps.commit.outputs.short }}
infisical/frontend:latest
infisical/frontend:${{ steps.extract_version.outputs.version }}
platforms: linux/amd64,linux/arm64
build-args: |
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}

View File

@@ -1,17 +1,11 @@
name: Build, Publish and Deploy to Gamma name: Build, Publish and Deploy to Gamma
on: on: [workflow_dispatch]
push:
tags:
- "infisical/v*.*.*"
jobs: jobs:
backend-image: backend-image:
name: Build backend image name: Build backend image
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Extract version from tag
id: extract_version
run: echo "::set-output name=version::${GITHUB_REF_NAME#infisical/}"
- name: ☁️ Checkout source - name: ☁️ Checkout source
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: 📦 Install dependencies to test all dependencies - name: 📦 Install dependencies to test all dependencies
@@ -57,18 +51,14 @@ jobs:
push: true push: true
context: backend context: backend
tags: | tags: |
infisical/backend:${{ steps.commit.outputs.short }} infisical/staging_deployment_backend:${{ steps.commit.outputs.short }}
infisical/backend:latest infisical/staging_deployment_backend:latest
infisical/backend:${{ steps.extract_version.outputs.version }}
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
frontend-image: frontend-image:
name: Build frontend image name: Build frontend image
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Extract version from tag
id: extract_version
run: echo "::set-output name=version::${GITHUB_REF_NAME#infisical/}"
- name: ☁️ Checkout source - name: ☁️ Checkout source
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Save commit hashes for tag - name: Save commit hashes for tag
@@ -90,12 +80,12 @@ jobs:
token: ${{ secrets.DEPOT_PROJECT_TOKEN }} token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
project: 64mmf0n610 project: 64mmf0n610
context: frontend context: frontend
tags: infisical/frontend:test tags: infisical/staging_deployment_frontend:test
build-args: | build-args: |
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }} POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
- name: ⏻ Spawn frontend container - name: ⏻ Spawn frontend container
run: | run: |
docker run -d --rm --name infisical-frontend-test infisical/frontend:test docker run -d --rm --name infisical-frontend-test infisical/staging_deployment_frontend:test
- name: 🧪 Test frontend image - name: 🧪 Test frontend image
run: | run: |
./.github/resources/healthcheck.sh infisical-frontend-test ./.github/resources/healthcheck.sh infisical-frontend-test
@@ -110,9 +100,8 @@ jobs:
token: ${{ secrets.DEPOT_PROJECT_TOKEN }} token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
context: frontend context: frontend
tags: | tags: |
infisical/frontend:${{ steps.commit.outputs.short }} infisical/staging_deployment_frontend:${{ steps.commit.outputs.short }}
infisical/frontend:latest infisical/staging_deployment_frontend:latest
infisical/frontend:${{ steps.extract_version.outputs.version }}
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
build-args: | build-args: |
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }} POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
@@ -146,7 +135,7 @@ jobs:
- name: Download helm values to file and upgrade gamma deploy - name: Download helm values to file and upgrade gamma deploy
run: | run: |
wget https://raw.githubusercontent.com/Infisical/infisical/main/.github/values.yaml wget https://raw.githubusercontent.com/Infisical/infisical/main/.github/values.yaml
helm upgrade infisical infisical-helm-charts/infisical --values values.yaml --recreate-pods helm upgrade infisical infisical-helm-charts/infisical --values values.yaml --wait
if [[ $(helm status infisical) == *"FAILED"* ]]; then if [[ $(helm status infisical) == *"FAILED"* ]]; then
echo "Helm upgrade failed" echo "Helm upgrade failed"
exit 1 exit 1

View File

@@ -25,7 +25,7 @@
<img src="https://img.shields.io/github/commit-activity/m/infisical/infisical" alt="git commit activity" /> <img src="https://img.shields.io/github/commit-activity/m/infisical/infisical" alt="git commit activity" />
</a> </a>
<a href="https://cloudsmith.io/~infisical/repos/"> <a href="https://cloudsmith.io/~infisical/repos/">
<img src="https://img.shields.io/badge/Downloads-240.2k-orange" alt="Cloudsmith downloads" /> <img src="https://img.shields.io/badge/Downloads-305.8k-orange" alt="Cloudsmith downloads" />
</a> </a>
<a href="https://join.slack.com/t/infisical-users/shared_invite/zt-1wehzfnzn-1aMo5JcGENJiNAC2SD8Jlg"> <a href="https://join.slack.com/t/infisical-users/shared_invite/zt-1wehzfnzn-1aMo5JcGENJiNAC2SD8Jlg">
<img src="https://img.shields.io/badge/chat-on%20Slack-blueviolet" alt="Slack community channel" /> <img src="https://img.shields.io/badge/chat-on%20Slack-blueviolet" alt="Slack community channel" />
@@ -127,7 +127,7 @@ Whether it's big or small, we love contributions. Check out our guide to see how
Not sure where to get started? You can: Not sure where to get started? You can:
- [Book a free, non-pressure pairing sessions with one of our teammates](mailto:tony@infisical.com?subject=Pairing%20session&body=I'd%20like%20to%20do%20a%20pairing%20session!)! - [Book a free, non-pressure pairing session / code walkthrough with one of our teammates](https://cal.com/tony-infisical/30-min-meeting-contributing)!
- Join our <a href="https://join.slack.com/t/infisical-users/shared_invite/zt-1wehzfnzn-1aMo5JcGENJiNAC2SD8Jlg">Slack</a>, and ask us any questions there. - Join our <a href="https://join.slack.com/t/infisical-users/shared_invite/zt-1wehzfnzn-1aMo5JcGENJiNAC2SD8Jlg">Slack</a>, and ask us any questions there.
## Resources ## Resources

View File

@@ -151,6 +151,7 @@ export const getIntegrationAuthApps = async (req: Request, res: Response) => {
const apps = await getApps({ const apps = await getApps({
integrationAuth: req.integrationAuth, integrationAuth: req.integrationAuth,
accessToken: req.accessToken, accessToken: req.accessToken,
accessId: req.accessId,
...teamId && { teamId } ...teamId && { teamId }
}); });

View File

@@ -1,10 +1,11 @@
import { Request, Response } from 'express'; import { Request, Response } from "express";
import { Types } from 'mongoose'; import { Types } from "mongoose";
import { import { Integration } from "../../models";
Integration import { EventService } from "../../services";
} from '../../models'; import { eventPushSecrets } from "../../events";
import { EventService } from '../../services'; import Folder from "../../models/folder";
import { eventPushSecrets } from '../../events'; import { getFolderByPath } from "../../services/FolderService";
import { BadRequestError } from "../../utils/errors";
/** /**
* Create/initialize an (empty) integration for integration authorization * Create/initialize an (empty) integration for integration authorization
@@ -25,9 +26,24 @@ export const createIntegration = async (req: Request, res: Response) => {
targetServiceId, targetServiceId,
owner, owner,
path, path,
region region,
secretPath,
} = req.body; } = req.body;
const folders = await Folder.findOne({
workspace: req.integrationAuth.workspace._id,
environment: sourceEnvironment,
});
if (folders) {
const folder = getFolderByPath(folders.nodes, secretPath);
if (!folder) {
throw BadRequestError({
message: "Path for service token does not exist",
});
}
}
// TODO: validate [sourceEnvironment] and [targetEnvironment] // TODO: validate [sourceEnvironment] and [targetEnvironment]
// initialize new integration after saving integration access token // initialize new integration after saving integration access token
@@ -44,17 +60,18 @@ export const createIntegration = async (req: Request, res: Response) => {
owner, owner,
path, path,
region, region,
secretPath,
integration: req.integrationAuth.integration, integration: req.integrationAuth.integration,
integrationAuth: new Types.ObjectId(integrationAuthId) integrationAuth: new Types.ObjectId(integrationAuthId),
}).save(); }).save();
if (integration) { if (integration) {
// trigger event - push secrets // trigger event - push secrets
EventService.handleEvent({ EventService.handleEvent({
event: eventPushSecrets({ event: eventPushSecrets({
workspaceId: integration.workspace, workspaceId: integration.workspace,
environment: sourceEnvironment environment: sourceEnvironment,
}) }),
}); });
} }
@@ -70,7 +87,6 @@ export const createIntegration = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const updateIntegration = async (req: Request, res: Response) => { export const updateIntegration = async (req: Request, res: Response) => {
// TODO: add integration-specific validation to ensure that each // TODO: add integration-specific validation to ensure that each
// integration has the correct fields populated in [Integration] // integration has the correct fields populated in [Integration]
@@ -81,8 +97,23 @@ export const updateIntegration = async (req: Request, res: Response) => {
appId, appId,
targetEnvironment, targetEnvironment,
owner, // github-specific integration param owner, // github-specific integration param
secretPath,
} = req.body; } = req.body;
const folders = await Folder.findOne({
workspace: req.integration.workspace,
environment,
});
if (folders) {
const folder = getFolderByPath(folders.nodes, secretPath);
if (!folder) {
throw BadRequestError({
message: "Path for service token does not exist",
});
}
}
const integration = await Integration.findOneAndUpdate( const integration = await Integration.findOneAndUpdate(
{ {
_id: req.integration._id, _id: req.integration._id,
@@ -94,6 +125,7 @@ export const updateIntegration = async (req: Request, res: Response) => {
appId, appId,
targetEnvironment, targetEnvironment,
owner, owner,
secretPath,
}, },
{ {
new: true, new: true,
@@ -105,7 +137,7 @@ export const updateIntegration = async (req: Request, res: Response) => {
EventService.handleEvent({ EventService.handleEvent({
event: eventPushSecrets({ event: eventPushSecrets({
workspaceId: integration.workspace, workspaceId: integration.workspace,
environment environment,
}), }),
}); });
} }

View File

@@ -99,7 +99,7 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => {
throw new Error('Failed to validate organization membership'); throw new Error('Failed to validate organization membership');
} }
const plan = await EELicenseService.getOrganizationPlan(organizationId); const plan = await EELicenseService.getPlan(organizationId);
if (plan.memberLimit !== null) { if (plan.memberLimit !== null) {
// case: limit imposed on number of members allowed // case: limit imposed on number of members allowed

View File

@@ -36,8 +36,8 @@ export const emailPasswordReset = async (req: Request, res: Response) => {
if (!user || !user?.publicKey) { if (!user || !user?.publicKey) {
// case: user has already completed account // case: user has already completed account
return res.status(403).send({ return res.status(200).send({
message: "If an account exists with this email, a password reset link has been sent" message:"If an account exists with this email, a password reset link has been sent"
}); });
} }

View File

@@ -11,6 +11,7 @@ import {
validateFolderName, validateFolderName,
generateFolderId, generateFolderId,
getParentFromFolderId, getParentFromFolderId,
getFolderByPath,
} from "../../services/FolderService"; } from "../../services/FolderService";
import { ADMIN, MEMBER } from "../../variables"; import { ADMIN, MEMBER } from "../../variables";
import { validateMembership } from "../../helpers/membership"; import { validateMembership } from "../../helpers/membership";
@@ -177,11 +178,13 @@ export const deleteFolder = async (req: Request, res: Response) => {
// TODO: validate workspace // TODO: validate workspace
export const getFolders = async (req: Request, res: Response) => { export const getFolders = async (req: Request, res: Response) => {
const { workspaceId, environment, parentFolderId } = req.query as { const { workspaceId, environment, parentFolderId, parentFolderPath } =
workspaceId: string; req.query as {
environment: string; workspaceId: string;
parentFolderId?: string; environment: string;
}; parentFolderId?: string;
parentFolderPath?: string;
};
const folders = await Folder.findOne({ workspace: workspaceId, environment }); const folders = await Folder.findOne({ workspace: workspaceId, environment });
if (!folders) { if (!folders) {
@@ -196,6 +199,20 @@ export const getFolders = async (req: Request, res: Response) => {
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
}); });
// if instead of parentFolderId given a path like /folder1/folder2
if (parentFolderPath) {
const folder = getFolderByPath(folders.nodes, parentFolderPath);
if (!folder) {
res.send({ folders: [], dir: [] });
return;
}
// dir is not needed at present as this is only used in overview section of secrets
res.send({
folders: folder.children.map(({ id, name }) => ({ id, name })),
dir: [{ name: folder.name, id: folder.id }],
});
}
if (!parentFolderId) { if (!parentFolderId) {
const rootFolders = folders.nodes.children.map(({ id, name }) => ({ const rootFolders = folders.nodes.children.map(({ id, name }) => ({
id, id,

View File

@@ -116,7 +116,7 @@ export const createWorkspace = async (req: Request, res: Response) => {
throw new Error("Failed to validate organization membership"); throw new Error("Failed to validate organization membership");
} }
const plan = await EELicenseService.getOrganizationPlan(organizationId); const plan = await EELicenseService.getPlan(organizationId);
if (plan.workspaceLimit !== null) { if (plan.workspaceLimit !== null) {
// case: limit imposed on number of workspaces allowed // case: limit imposed on number of workspaces allowed

View File

@@ -8,7 +8,8 @@ import {
Membership, Membership,
} from '../../models'; } from '../../models';
import { SecretVersion } from '../../ee/models'; import { SecretVersion } from '../../ee/models';
import { BadRequestError } from '../../utils/errors'; import { EELicenseService } from '../../ee/services';
import { BadRequestError, WorkspaceNotFoundError } from '../../utils/errors';
import _ from 'lodash'; import _ from 'lodash';
import { PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS } from '../../variables'; import { PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS } from '../../variables';
@@ -22,9 +23,26 @@ export const createWorkspaceEnvironment = async (
req: Request, req: Request,
res: Response res: Response
) => { ) => {
const { workspaceId } = req.params; const { workspaceId } = req.params;
const { environmentName, environmentSlug } = req.body; const { environmentName, environmentSlug } = req.body;
const workspace = await Workspace.findById(workspaceId).exec(); const workspace = await Workspace.findById(workspaceId).exec();
if (!workspace) throw WorkspaceNotFoundError();
const plan = await EELicenseService.getPlan(workspace.organization.toString());
if (plan.environmentLimit !== null) {
// case: limit imposed on number of environments allowed
if (workspace.environments.length >= plan.environmentLimit) {
// case: number of environments used exceeds the number of environments allowed
return res.status(400).send({
message: 'Failed to create environment due to environment limit reached. Upgrade plan to create more environments.'
});
}
}
if ( if (
!workspace || !workspace ||
workspace?.environments.find( workspace?.environments.find(
@@ -40,6 +58,8 @@ export const createWorkspaceEnvironment = async (
}); });
await workspace.save(); await workspace.save();
await EELicenseService.refreshPlan(workspace.organization.toString(), workspaceId);
return res.status(200).send({ return res.status(200).send({
message: 'Successfully created new environment', message: 'Successfully created new environment',
workspace: workspaceId, workspace: workspaceId,
@@ -186,7 +206,9 @@ export const deleteWorkspaceEnvironment = async (
await Membership.updateMany( await Membership.updateMany(
{ workspace: workspaceId }, { workspace: workspaceId },
{ $pull: { deniedPermissions: { environmentSlug: environmentSlug } } } { $pull: { deniedPermissions: { environmentSlug: environmentSlug } } }
) );
await EELicenseService.refreshPlan(workspace.organization.toString(), workspaceId);
return res.status(200).send({ return res.status(200).send({
message: 'Successfully deleted environment', message: 'Successfully deleted environment',

View File

@@ -700,11 +700,15 @@ export const getSecrets = async (req: Request, res: Response) => {
(!folders && folderId && folderId !== "root") || (!folders && folderId && folderId !== "root") ||
(!folders && secretPath) (!folders && secretPath)
) { ) {
throw BadRequestError({ message: "Folder not found" }); res.send({ secrets: [] });
return;
} }
if (folders && folderId !== "root") { if (folders && folderId !== "root") {
const folder = searchByFolderId(folders.nodes, folderId as string); const folder = searchByFolderId(folders.nodes, folderId as string);
if (!folder) throw BadRequestError({ message: "Folder not found" }); if (!folder) {
res.send({ secrets: [] });
return;
}
} }
if (req.authData.authPayload instanceof ServiceTokenData) { if (req.authData.authPayload instanceof ServiceTokenData) {
@@ -720,10 +724,11 @@ export const getSecrets = async (req: Request, res: Response) => {
} }
if (folders && secretPath) { if (folders && secretPath) {
if (!folders) throw BadRequestError({ message: "Folder not found" }); // avoid throwing error and send empty list
const folder = getFolderByPath(folders.nodes, secretPath as string); const folder = getFolderByPath(folders.nodes, secretPath as string);
if (!folder) { if (!folder) {
throw BadRequestError({ message: "Secret path not found" }); res.send({ secrets: [] });
return;
} }
folderId = folder.id; folderId = folder.id;
} }

View File

@@ -34,7 +34,7 @@ export const getSecretsRaw = async (req: Request, res: Response) => {
secret, secret,
key key
}); });
return rep; return rep;
}) })
}); });
@@ -88,7 +88,7 @@ export const createSecretRaw = async (req: Request, res: Response) => {
secretComment, secretComment,
secretPath = "/" secretPath = "/"
} = req.body; } = req.body;
const key = await BotService.getWorkspaceKeyWithBot({ const key = await BotService.getWorkspaceKeyWithBot({
workspaceId: new Types.ObjectId(workspaceId) workspaceId: new Types.ObjectId(workspaceId)
}); });
@@ -102,12 +102,12 @@ export const createSecretRaw = async (req: Request, res: Response) => {
plaintext: secretValue, plaintext: secretValue,
key key
}); });
const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8({ const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8({
plaintext: secretComment, plaintext: secretComment,
key key
}); });
const secret = await SecretService.createSecret({ const secret = await SecretService.createSecret({
secretName, secretName,
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
@@ -135,7 +135,7 @@ export const createSecretRaw = async (req: Request, res: Response) => {
const secretWithoutBlindIndex = secret.toObject(); const secretWithoutBlindIndex = secret.toObject();
delete secretWithoutBlindIndex.secretBlindIndex; delete secretWithoutBlindIndex.secretBlindIndex;
return res.status(200).send({ return res.status(200).send({
secret: repackageSecretToRaw({ secret: repackageSecretToRaw({
secret: secretWithoutBlindIndex, secret: secretWithoutBlindIndex,
@@ -202,11 +202,11 @@ export const updateSecretByNameRaw = async (req: Request, res: Response) => {
*/ */
export const deleteSecretByNameRaw = async (req: Request, res: Response) => { export const deleteSecretByNameRaw = async (req: Request, res: Response) => {
const { secretName } = req.params; const { secretName } = req.params;
const { const {
workspaceId, workspaceId,
environment, environment,
type, type,
secretPath = "/" secretPath = "/"
} = req.body; } = req.body;
const { secret } = await SecretService.deleteSecret({ const { secret } = await SecretService.deleteSecret({
@@ -391,11 +391,11 @@ export const updateSecretByName = async (req: Request, res: Response) => {
*/ */
export const deleteSecretByName = async (req: Request, res: Response) => { export const deleteSecretByName = async (req: Request, res: Response) => {
const { secretName } = req.params; const { secretName } = req.params;
const { const {
workspaceId, workspaceId,
environment, environment,
type, type,
secretPath = "/" secretPath = "/"
} = req.body; } = req.body;
const { secret } = await SecretService.deleteSecret({ const { secret } = await SecretService.deleteSecret({

View File

@@ -8,8 +8,9 @@ import { EELicenseService } from '../../services';
*/ */
export const getOrganizationPlan = async (req: Request, res: Response) => { export const getOrganizationPlan = async (req: Request, res: Response) => {
const { organizationId } = req.params; const { organizationId } = req.params;
const workspaceId = req.query.workspaceId as string;
const plan = await EELicenseService.getOrganizationPlan(organizationId); const plan = await EELicenseService.getPlan(organizationId, workspaceId);
return res.status(200).send({ return res.status(200).send({
plan, plan,

View File

@@ -5,7 +5,7 @@ import {
requireOrganizationAuth, requireOrganizationAuth,
validateRequest validateRequest
} from '../../../middleware'; } from '../../../middleware';
import { param, body } from 'express-validator'; import { param, body, query } from 'express-validator';
import { organizationsController } from '../../controllers/v1'; import { organizationsController } from '../../controllers/v1';
import { import {
OWNER, ADMIN, MEMBER, ACCEPTED OWNER, ADMIN, MEMBER, ACCEPTED
@@ -21,6 +21,7 @@ router.get(
acceptedStatuses: [ACCEPTED] acceptedStatuses: [ACCEPTED]
}), }),
param('organizationId').exists().trim(), param('organizationId').exists().trim(),
query('workspaceId').optional().isString(),
validateRequest, validateRequest,
organizationsController.getOrganizationPlan organizationsController.getOrganizationPlan
); );

View File

@@ -22,13 +22,14 @@ interface FeatureSet {
workspacesUsed: number; workspacesUsed: number;
memberLimit: number | null; memberLimit: number | null;
membersUsed: number; membersUsed: number;
environmentLimit: number | null;
environmentsUsed: number;
secretVersioning: boolean; secretVersioning: boolean;
pitRecovery: boolean; pitRecovery: boolean;
rbac: boolean; rbac: boolean;
customRateLimits: boolean; customRateLimits: boolean;
customAlerts: boolean; customAlerts: boolean;
auditLogs: boolean; auditLogs: boolean;
envLimit?: number | null;
} }
/** /**
@@ -51,13 +52,14 @@ class EELicenseService {
workspacesUsed: 0, workspacesUsed: 0,
memberLimit: null, memberLimit: null,
membersUsed: 0, membersUsed: 0,
environmentLimit: null,
environmentsUsed: 0,
secretVersioning: true, secretVersioning: true,
pitRecovery: true, pitRecovery: true,
rbac: true, rbac: true,
customRateLimits: true, customRateLimits: true,
customAlerts: true, customAlerts: true,
auditLogs: false, auditLogs: false
envLimit: null
} }
public localFeatureSet: NodeCache; public localFeatureSet: NodeCache;
@@ -69,10 +71,10 @@ class EELicenseService {
}); });
} }
public async getOrganizationPlan(organizationId: string): Promise<FeatureSet> { public async getPlan(organizationId: string, workspaceId?: string): Promise<FeatureSet> {
try { try {
if (this.instanceType === 'cloud') { if (this.instanceType === 'cloud') {
const cachedPlan = this.localFeatureSet.get<FeatureSet>(organizationId); const cachedPlan = this.localFeatureSet.get<FeatureSet>(`${organizationId}-${workspaceId ?? ''}`);
if (cachedPlan) { if (cachedPlan) {
return cachedPlan; return cachedPlan;
} }
@@ -80,12 +82,16 @@ class EELicenseService {
const organization = await Organization.findById(organizationId); const organization = await Organization.findById(organizationId);
if (!organization) throw OrganizationNotFoundError(); if (!organization) throw OrganizationNotFoundError();
const { data: { currentPlan } } = await licenseServerKeyRequest.get( let url = `${await getLicenseServerUrl()}/api/license-server/v1/customers/${organization.customerId}/cloud-plan`;
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${organization.customerId}/cloud-plan`
); if (workspaceId) {
url += `?workspaceId=${workspaceId}`;
}
const { data: { currentPlan } } = await licenseServerKeyRequest.get(url);
// cache fetched plan for organization // cache fetched plan for organization
this.localFeatureSet.set(organizationId, currentPlan); this.localFeatureSet.set(`${organizationId}-${workspaceId ?? ''}`, currentPlan);
return currentPlan; return currentPlan;
} }
@@ -96,10 +102,10 @@ class EELicenseService {
return this.globalFeatureSet; return this.globalFeatureSet;
} }
public async refreshOrganizationPlan(organizationId: string) { public async refreshPlan(organizationId: string, workspaceId?: string) {
if (this.instanceType === 'cloud') { if (this.instanceType === 'cloud') {
this.localFeatureSet.del(organizationId); this.localFeatureSet.del(`${organizationId}-${workspaceId ?? ''}`);
await this.getOrganizationPlan(organizationId); await this.getPlan(organizationId, workspaceId);
} }
} }

View File

@@ -1,29 +1,21 @@
import { Types } from "mongoose"; import { Types } from "mongoose";
import { import { Bot, BotKey, Secret, ISecret, IUser } from "../models";
Bot,
BotKey,
Secret,
ISecret,
IUser
} from "../models";
import { import {
generateKeyPair, generateKeyPair,
encryptSymmetric128BitHexKeyUTF8, encryptSymmetric128BitHexKeyUTF8,
decryptSymmetric128BitHexKeyUTF8, decryptSymmetric128BitHexKeyUTF8,
decryptAsymmetric decryptAsymmetric,
} from '../utils/crypto'; } from "../utils/crypto";
import { import {
SECRET_SHARED, SECRET_SHARED,
ALGORITHM_AES_256_GCM, ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_UTF8, ENCODING_SCHEME_UTF8,
ENCODING_SCHEME_BASE64 ENCODING_SCHEME_BASE64,
} from "../variables"; } from "../variables";
import { import { getEncryptionKey, getRootEncryptionKey, client } from "../config";
getEncryptionKey,
getRootEncryptionKey,
client
} from "../config";
import { InternalServerError } from "../utils/errors"; import { InternalServerError } from "../utils/errors";
import Folder from "../models/folder";
import { getFolderByPath } from "../services/FolderService";
/** /**
* Create an inactive bot with name [name] for workspace with id [workspaceId] * Create an inactive bot with name [name] for workspace with id [workspaceId]
@@ -40,15 +32,14 @@ export const createBot = async ({
}) => { }) => {
const encryptionKey = await getEncryptionKey(); const encryptionKey = await getEncryptionKey();
const rootEncryptionKey = await getRootEncryptionKey(); const rootEncryptionKey = await getRootEncryptionKey();
const { publicKey, privateKey } = generateKeyPair(); const { publicKey, privateKey } = generateKeyPair();
if (rootEncryptionKey) { if (rootEncryptionKey) {
const { const { ciphertext, iv, tag } = client.encryptSymmetric(
ciphertext, privateKey,
iv, rootEncryptionKey
tag );
} = client.encryptSymmetric(privateKey, rootEncryptionKey);
return await new Bot({ return await new Bot({
name, name,
@@ -59,9 +50,8 @@ export const createBot = async ({
iv, iv,
tag, tag,
algorithm: ALGORITHM_AES_256_GCM, algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_BASE64 keyEncoding: ENCODING_SCHEME_BASE64,
}).save(); }).save();
} else if (encryptionKey) { } else if (encryptionKey) {
const { ciphertext, iv, tag } = encryptSymmetric128BitHexKeyUTF8({ const { ciphertext, iv, tag } = encryptSymmetric128BitHexKeyUTF8({
plaintext: privateKey, plaintext: privateKey,
@@ -77,12 +67,12 @@ export const createBot = async ({
iv, iv,
tag, tag,
algorithm: ALGORITHM_AES_256_GCM, algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8 keyEncoding: ENCODING_SCHEME_UTF8,
}).save(); }).save();
} }
throw InternalServerError({ throw InternalServerError({
message: 'Failed to create new bot due to missing encryption key' message: "Failed to create new bot due to missing encryption key",
}); });
}; };
@@ -92,11 +82,11 @@ export const createBot = async ({
*/ */
export const getIsWorkspaceE2EEHelper = async (workspaceId: Types.ObjectId) => { export const getIsWorkspaceE2EEHelper = async (workspaceId: Types.ObjectId) => {
const botKey = await BotKey.exists({ const botKey = await BotKey.exists({
workspace: workspaceId workspace: workspaceId,
}); });
return botKey ? false : true; return botKey ? false : true;
} };
/** /**
* Return decrypted secrets for workspace with id [workspaceId] * Return decrypted secrets for workspace with id [workspaceId]
@@ -108,16 +98,38 @@ export const getIsWorkspaceE2EEHelper = async (workspaceId: Types.ObjectId) => {
export const getSecretsBotHelper = async ({ export const getSecretsBotHelper = async ({
workspaceId, workspaceId,
environment, environment,
secretPath,
}: { }: {
workspaceId: Types.ObjectId; workspaceId: Types.ObjectId;
environment: string; environment: string;
secretPath: string;
}) => { }) => {
const content = {} as any; const content = {} as any;
const key = await getKey({ workspaceId: workspaceId }); const key = await getKey({ workspaceId: workspaceId });
let folderId = "root";
const folders = await Folder.findOne({
workspace: workspaceId,
environment,
});
if (!folders && secretPath !== "/") {
throw InternalServerError({ message: "Folder not found" });
}
if (folders) {
const folder = getFolderByPath(folders.nodes, secretPath);
if (!folder) {
throw InternalServerError({ message: "Folder not found" });
}
folderId = folder.id;
}
const secrets = await Secret.find({ const secrets = await Secret.find({
workspace: workspaceId, workspace: workspaceId,
environment, environment,
type: SECRET_SHARED, type: SECRET_SHARED,
folder: folderId,
}); });
secrets.forEach((secret: ISecret) => { secrets.forEach((secret: ISecret) => {
@@ -148,14 +160,17 @@ export const getSecretsBotHelper = async ({
* @param {String} obj.workspaceId - id of workspace * @param {String} obj.workspaceId - id of workspace
* @returns {String} key - decrypted workspace key * @returns {String} key - decrypted workspace key
*/ */
export const getKey = async ({ workspaceId }: { workspaceId: Types.ObjectId }) => { export const getKey = async ({
workspaceId,
}: {
workspaceId: Types.ObjectId;
}) => {
const encryptionKey = await getEncryptionKey(); const encryptionKey = await getEncryptionKey();
const rootEncryptionKey = await getRootEncryptionKey(); const rootEncryptionKey = await getRootEncryptionKey();
const botKey = await BotKey.findOne({ const botKey = await BotKey.findOne({
workspace: workspaceId, workspace: workspaceId,
}) }).populate<{ sender: IUser }>("sender", "publicKey");
.populate<{ sender: IUser }>("sender", "publicKey");
if (!botKey) throw new Error("Failed to find bot key"); if (!botKey) throw new Error("Failed to find bot key");
@@ -168,7 +183,12 @@ export const getKey = async ({ workspaceId }: { workspaceId: Types.ObjectId }) =
if (rootEncryptionKey && bot.keyEncoding === ENCODING_SCHEME_BASE64) { if (rootEncryptionKey && bot.keyEncoding === ENCODING_SCHEME_BASE64) {
// case: encoding scheme is base64 // case: encoding scheme is base64
const privateKeyBot = client.decryptSymmetric(bot.encryptedPrivateKey, rootEncryptionKey, bot.iv, bot.tag); const privateKeyBot = client.decryptSymmetric(
bot.encryptedPrivateKey,
rootEncryptionKey,
bot.iv,
bot.tag
);
return decryptAsymmetric({ return decryptAsymmetric({
ciphertext: botKey.encryptedKey, ciphertext: botKey.encryptedKey,
@@ -177,15 +197,14 @@ export const getKey = async ({ workspaceId }: { workspaceId: Types.ObjectId }) =
privateKey: privateKeyBot, privateKey: privateKeyBot,
}); });
} else if (encryptionKey && bot.keyEncoding === ENCODING_SCHEME_UTF8) { } else if (encryptionKey && bot.keyEncoding === ENCODING_SCHEME_UTF8) {
// case: encoding scheme is utf8 // case: encoding scheme is utf8
const privateKeyBot = decryptSymmetric128BitHexKeyUTF8({ const privateKeyBot = decryptSymmetric128BitHexKeyUTF8({
ciphertext: bot.encryptedPrivateKey, ciphertext: bot.encryptedPrivateKey,
iv: bot.iv, iv: bot.iv,
tag: bot.tag, tag: bot.tag,
key: encryptionKey key: encryptionKey,
}); });
return decryptAsymmetric({ return decryptAsymmetric({
ciphertext: botKey.encryptedKey, ciphertext: botKey.encryptedKey,
nonce: botKey.nonce, nonce: botKey.nonce,
@@ -195,7 +214,8 @@ export const getKey = async ({ workspaceId }: { workspaceId: Types.ObjectId }) =
} }
throw InternalServerError({ throw InternalServerError({
message: "Failed to obtain bot's copy of workspace key needed for bot operations" message:
"Failed to obtain bot's copy of workspace key needed for bot operations",
}); });
}; };
@@ -254,4 +274,4 @@ export const decryptSymmetricHelper = async ({
}); });
return plaintext; return plaintext;
}; };

View File

@@ -1,26 +1,20 @@
import { Types } from 'mongoose'; import { Types } from "mongoose";
import { Bot, Integration, IntegrationAuth } from "../models";
import { exchangeCode, exchangeRefresh, syncSecrets } from "../integrations";
import { BotService } from "../services";
import { import {
Bot, INTEGRATION_VERCEL,
Integration, INTEGRATION_NETLIFY,
IntegrationAuth ALGORITHM_AES_256_GCM,
} from '../models'; ENCODING_SCHEME_UTF8,
import { exchangeCode, exchangeRefresh, syncSecrets } from '../integrations'; } from "../variables";
import { BotService } from '../services'; import { UnauthorizedRequestError } from "../utils/errors";
import {
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_UTF8
} from '../variables';
import {
UnauthorizedRequestError,
} from '../utils/errors';
interface Update { interface Update {
workspace: string; workspace: string;
integration: string; integration: string;
teamId?: string; teamId?: string;
accountId?: string; accountId?: string;
} }
/** /**
@@ -31,78 +25,83 @@ interface Update {
* - Create bot sequence for integration * - Create bot sequence for integration
* @param {Object} obj * @param {Object} obj
* @param {String} obj.workspaceId - id of workspace * @param {String} obj.workspaceId - id of workspace
* @param {String} obj.integration - name of integration * @param {String} obj.integration - name of integration
* @param {String} obj.code - code * @param {String} obj.code - code
* @returns {IntegrationAuth} integrationAuth - integration auth after OAuth2 code-token exchange * @returns {IntegrationAuth} integrationAuth - integration auth after OAuth2 code-token exchange
*/ */
export const handleOAuthExchangeHelper = async ({ export const handleOAuthExchangeHelper = async ({
workspaceId, workspaceId,
integration,
code,
environment,
}: {
workspaceId: string;
integration: string;
code: string;
environment: string;
}) => {
const bot = await Bot.findOne({
workspace: workspaceId,
isActive: true,
});
if (!bot)
throw new Error("Bot must be enabled for OAuth2 code-token exchange");
// exchange code for access and refresh tokens
const res = await exchangeCode({
integration, integration,
code, code,
environment });
}: {
workspaceId: string; const update: Update = {
integration: string; workspace: workspaceId,
code: string; integration,
environment: string; };
}) => {
const bot = await Bot.findOne({ switch (integration) {
workspace: workspaceId, case INTEGRATION_VERCEL:
isActive: true update.teamId = res.teamId;
break;
case INTEGRATION_NETLIFY:
update.accountId = res.accountId;
break;
}
const integrationAuth = await IntegrationAuth.findOneAndUpdate(
{
workspace: workspaceId,
integration,
},
update,
{
new: true,
upsert: true,
}
);
if (res.refreshToken) {
// case: refresh token returned from exchange
// set integration auth refresh token
await setIntegrationAuthRefreshHelper({
integrationAuthId: integrationAuth._id.toString(),
refreshToken: res.refreshToken,
}); });
}
if (!bot) throw new Error('Bot must be enabled for OAuth2 code-token exchange');
if (res.accessToken) {
// exchange code for access and refresh tokens // case: access token returned from exchange
const res = await exchangeCode({ // set integration auth access token
integration, await setIntegrationAuthAccessHelper({
code integrationAuthId: integrationAuth._id.toString(),
accessId: null,
accessToken: res.accessToken,
accessExpiresAt: res.accessExpiresAt,
}); });
}
const update: Update = {
workspace: workspaceId, return integrationAuth;
integration };
}
switch (integration) {
case INTEGRATION_VERCEL:
update.teamId = res.teamId;
break;
case INTEGRATION_NETLIFY:
update.accountId = res.accountId;
break;
}
const integrationAuth = await IntegrationAuth.findOneAndUpdate({
workspace: workspaceId,
integration
}, update, {
new: true,
upsert: true
});
if (res.refreshToken) {
// case: refresh token returned from exchange
// set integration auth refresh token
await setIntegrationAuthRefreshHelper({
integrationAuthId: integrationAuth._id.toString(),
refreshToken: res.refreshToken
});
}
if (res.accessToken) {
// case: access token returned from exchange
// set integration auth access token
await setIntegrationAuthAccessHelper({
integrationAuthId: integrationAuth._id.toString(),
accessId: null,
accessToken: res.accessToken,
accessExpiresAt: res.accessExpiresAt
});
}
return integrationAuth;
}
/** /**
* Sync/push environment variables in workspace with id [workspaceId] to * Sync/push environment variables in workspace with id [workspaceId] to
* all active integrations for that workspace * all active integrations for that workspace
@@ -110,48 +109,54 @@ export const handleOAuthExchangeHelper = async ({
* @param {Object} obj.workspaceId - id of workspace * @param {Object} obj.workspaceId - id of workspace
*/ */
export const syncIntegrationsHelper = async ({ export const syncIntegrationsHelper = async ({
workspaceId, workspaceId,
environment environment,
}: { }: {
workspaceId: Types.ObjectId; workspaceId: Types.ObjectId;
environment?: string; environment?: string;
}) => { }) => {
const integrations = await Integration.find({ const integrations = await Integration.find({
workspace: workspaceId, workspace: workspaceId,
...(environment ? { ...(environment
environment ? {
} : {}), environment,
isActive: true, }
app: { $ne: null } : {}),
isActive: true,
app: { $ne: null },
});
// for each workspace integration, sync/push secrets
// to that integration
for await (const integration of integrations) {
// get workspace, environment (shared) secrets
const secrets = await BotService.getSecrets({
// issue here?
workspaceId: integration.workspace,
environment: integration.environment,
secretPath: integration.secretPath,
}); });
// for each workspace integration, sync/push secrets const integrationAuth = await IntegrationAuth.findById(
// to that integration integration.integrationAuth
for await (const integration of integrations) { );
// get workspace, environment (shared) secrets if (!integrationAuth) throw new Error("Failed to find integration auth");
const secrets = await BotService.getSecrets({ // issue here?
workspaceId: integration.workspace,
environment: integration.environment
});
const integrationAuth = await IntegrationAuth.findById(integration.integrationAuth); // get integration auth access token
if (!integrationAuth) throw new Error('Failed to find integration auth'); const access = await getIntegrationAuthAccessHelper({
integrationAuthId: integration.integrationAuth,
// get integration auth access token });
const access = await getIntegrationAuthAccessHelper({
integrationAuthId: integration.integrationAuth
});
// sync secrets to integration // sync secrets to integration
await syncSecrets({ await syncSecrets({
integration, integration,
integrationAuth, integrationAuth,
secrets, secrets,
accessId: access.accessId === undefined ? null : access.accessId, accessId: access.accessId === undefined ? null : access.accessId,
accessToken: access.accessToken accessToken: access.accessToken,
}); });
} }
} };
/** /**
* Return decrypted refresh token using the bot's copy * Return decrypted refresh token using the bot's copy
@@ -161,22 +166,29 @@ export const syncIntegrationsHelper = async ({
* @param {String} obj.integrationAuthId - id of integration auth * @param {String} obj.integrationAuthId - id of integration auth
* @param {String} refreshToken - decrypted refresh token * @param {String} refreshToken - decrypted refresh token
*/ */
export const getIntegrationAuthRefreshHelper = async ({ integrationAuthId }: { integrationAuthId: Types.ObjectId }) => { export const getIntegrationAuthRefreshHelper = async ({
const integrationAuth = await IntegrationAuth integrationAuthId,
.findById(integrationAuthId) }: {
.select('+refreshCiphertext +refreshIV +refreshTag'); integrationAuthId: Types.ObjectId;
}) => {
const integrationAuth = await IntegrationAuth.findById(
integrationAuthId
).select("+refreshCiphertext +refreshIV +refreshTag");
if (!integrationAuth) throw UnauthorizedRequestError({message: 'Failed to locate Integration Authentication credentials'}); if (!integrationAuth)
throw UnauthorizedRequestError({
const refreshToken = await BotService.decryptSymmetric({ message: "Failed to locate Integration Authentication credentials",
workspaceId: integrationAuth.workspace,
ciphertext: integrationAuth.refreshCiphertext as string,
iv: integrationAuth.refreshIV as string,
tag: integrationAuth.refreshTag as string
}); });
return refreshToken; const refreshToken = await BotService.decryptSymmetric({
} workspaceId: integrationAuth.workspace,
ciphertext: integrationAuth.refreshCiphertext as string,
iv: integrationAuth.refreshIV as string,
tag: integrationAuth.refreshTag as string,
});
return refreshToken;
};
/** /**
* Return decrypted access token using the bot's copy * Return decrypted access token using the bot's copy
@@ -186,50 +198,65 @@ export const getIntegrationAuthRefreshHelper = async ({ integrationAuthId }: { i
* @param {String} obj.integrationAuthId - id of integration auth * @param {String} obj.integrationAuthId - id of integration auth
* @returns {String} accessToken - decrypted access token * @returns {String} accessToken - decrypted access token
*/ */
export const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrationAuthId: Types.ObjectId }) => { export const getIntegrationAuthAccessHelper = async ({
let accessId; integrationAuthId,
let accessToken; }: {
const integrationAuth = await IntegrationAuth integrationAuthId: Types.ObjectId;
.findById(integrationAuthId) }) => {
.select('workspace integration +accessCiphertext +accessIV +accessTag +accessExpiresAt + refreshCiphertext +accessIdCiphertext +accessIdIV +accessIdTag'); let accessId;
let accessToken;
const integrationAuth = await IntegrationAuth.findById(
integrationAuthId
).select(
"workspace integration +accessCiphertext +accessIV +accessTag +accessExpiresAt + refreshCiphertext +accessIdCiphertext +accessIdIV +accessIdTag"
);
if (!integrationAuth) throw UnauthorizedRequestError({message: 'Failed to locate Integration Authentication credentials'}); if (!integrationAuth)
throw UnauthorizedRequestError({
accessToken = await BotService.decryptSymmetric({ message: "Failed to locate Integration Authentication credentials",
workspaceId: integrationAuth.workspace,
ciphertext: integrationAuth.accessCiphertext as string,
iv: integrationAuth.accessIV as string,
tag: integrationAuth.accessTag as string
}); });
if (integrationAuth?.accessExpiresAt && integrationAuth?.refreshCiphertext) { accessToken = await BotService.decryptSymmetric({
// there is a access token expiration date workspaceId: integrationAuth.workspace,
// and refresh token to exchange with the OAuth2 server ciphertext: integrationAuth.accessCiphertext as string,
iv: integrationAuth.accessIV as string,
if (integrationAuth.accessExpiresAt < new Date()) { tag: integrationAuth.accessTag as string,
// access token is expired });
const refreshToken = await getIntegrationAuthRefreshHelper({ integrationAuthId });
accessToken = await exchangeRefresh({ if (integrationAuth?.accessExpiresAt && integrationAuth?.refreshCiphertext) {
integrationAuth, // there is a access token expiration date
refreshToken // and refresh token to exchange with the OAuth2 server
});
} if (integrationAuth.accessExpiresAt < new Date()) {
// access token is expired
const refreshToken = await getIntegrationAuthRefreshHelper({
integrationAuthId,
});
accessToken = await exchangeRefresh({
integrationAuth,
refreshToken,
});
} }
}
if (integrationAuth?.accessIdCiphertext && integrationAuth?.accessIdIV && integrationAuth?.accessIdTag) {
accessId = await BotService.decryptSymmetric({ if (
workspaceId: integrationAuth.workspace, integrationAuth?.accessIdCiphertext &&
ciphertext: integrationAuth.accessIdCiphertext as string, integrationAuth?.accessIdIV &&
iv: integrationAuth.accessIdIV as string, integrationAuth?.accessIdTag
tag: integrationAuth.accessIdTag as string ) {
}); accessId = await BotService.decryptSymmetric({
} workspaceId: integrationAuth.workspace,
ciphertext: integrationAuth.accessIdCiphertext as string,
return ({ iv: integrationAuth.accessIdIV as string,
accessId, tag: integrationAuth.accessIdTag as string,
accessToken
}); });
} }
return {
accessId,
accessToken,
};
};
/** /**
* Encrypt refresh token [refreshToken] using the bot's copy * Encrypt refresh token [refreshToken] using the bot's copy
@@ -240,41 +267,43 @@ export const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { in
* @param {String} obj.refreshToken - refresh token * @param {String} obj.refreshToken - refresh token
*/ */
export const setIntegrationAuthRefreshHelper = async ({ export const setIntegrationAuthRefreshHelper = async ({
integrationAuthId, integrationAuthId,
refreshToken refreshToken,
}: { }: {
integrationAuthId: string; integrationAuthId: string;
refreshToken: string; refreshToken: string;
}) => { }) => {
let integrationAuth = await IntegrationAuth.findById(integrationAuthId);
let integrationAuth = await IntegrationAuth
.findById(integrationAuthId); if (!integrationAuth) throw new Error("Failed to find integration auth");
if (!integrationAuth) throw new Error('Failed to find integration auth'); const obj = await BotService.encryptSymmetric({
workspaceId: integrationAuth.workspace,
const obj = await BotService.encryptSymmetric({ plaintext: refreshToken,
workspaceId: integrationAuth.workspace, });
plaintext: refreshToken
}); integrationAuth = await IntegrationAuth.findOneAndUpdate(
{
integrationAuth = await IntegrationAuth.findOneAndUpdate({ _id: integrationAuthId,
_id: integrationAuthId },
}, { {
refreshCiphertext: obj.ciphertext, refreshCiphertext: obj.ciphertext,
refreshIV: obj.iv, refreshIV: obj.iv,
refreshTag: obj.tag, refreshTag: obj.tag,
algorithm: ALGORITHM_AES_256_GCM, algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8 keyEncoding: ENCODING_SCHEME_UTF8,
}, { },
new: true {
}); new: true,
}
return integrationAuth; );
}
return integrationAuth;
};
/** /**
* Encrypt access token [accessToken] and (optionally) access id [accessId] * Encrypt access token [accessToken] and (optionally) access id [accessId]
* using the bot's copy of the workspace key for workspace belonging to * using the bot's copy of the workspace key for workspace belonging to
* integration auth with id [integrationAuthId] and store it along with [accessExpiresAt] * integration auth with id [integrationAuthId] and store it along with [accessExpiresAt]
* @param {Object} obj * @param {Object} obj
* @param {String} obj.integrationAuthId - id of integration auth * @param {String} obj.integrationAuthId - id of integration auth
@@ -282,48 +311,52 @@ export const setIntegrationAuthRefreshHelper = async ({
* @param {Date} obj.accessExpiresAt - expiration date of access token * @param {Date} obj.accessExpiresAt - expiration date of access token
*/ */
export const setIntegrationAuthAccessHelper = async ({ export const setIntegrationAuthAccessHelper = async ({
integrationAuthId, integrationAuthId,
accessId, accessId,
accessToken, accessToken,
accessExpiresAt accessExpiresAt,
}: { }: {
integrationAuthId: string; integrationAuthId: string;
accessId: string | null; accessId: string | null;
accessToken: string; accessToken: string;
accessExpiresAt: Date | undefined; accessExpiresAt: Date | undefined;
}) => { }) => {
let integrationAuth = await IntegrationAuth.findById(integrationAuthId); let integrationAuth = await IntegrationAuth.findById(integrationAuthId);
if (!integrationAuth) throw new Error('Failed to find integration auth'); if (!integrationAuth) throw new Error("Failed to find integration auth");
const encryptedAccessTokenObj = await BotService.encryptSymmetric({ const encryptedAccessTokenObj = await BotService.encryptSymmetric({
workspaceId: integrationAuth.workspace, workspaceId: integrationAuth.workspace,
plaintext: accessToken plaintext: accessToken,
});
let encryptedAccessIdObj;
if (accessId) {
encryptedAccessIdObj = await BotService.encryptSymmetric({
workspaceId: integrationAuth.workspace,
plaintext: accessId,
}); });
}
let encryptedAccessIdObj;
if (accessId) { integrationAuth = await IntegrationAuth.findOneAndUpdate(
encryptedAccessIdObj = await BotService.encryptSymmetric({ {
workspaceId: integrationAuth.workspace, _id: integrationAuthId,
plaintext: accessId },
}); {
accessIdCiphertext: encryptedAccessIdObj?.ciphertext ?? undefined,
accessIdIV: encryptedAccessIdObj?.iv ?? undefined,
accessIdTag: encryptedAccessIdObj?.tag ?? undefined,
accessCiphertext: encryptedAccessTokenObj.ciphertext,
accessIV: encryptedAccessTokenObj.iv,
accessTag: encryptedAccessTokenObj.tag,
accessExpiresAt,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8,
},
{
new: true,
} }
);
integrationAuth = await IntegrationAuth.findOneAndUpdate({
_id: integrationAuthId return integrationAuth;
}, { };
accessIdCiphertext: encryptedAccessIdObj?.ciphertext ?? undefined,
accessIdIV: encryptedAccessIdObj?.iv ?? undefined,
accessIdTag: encryptedAccessIdObj?.tag ?? undefined,
accessCiphertext: encryptedAccessTokenObj.ciphertext,
accessIV: encryptedAccessTokenObj.iv,
accessTag: encryptedAccessTokenObj.tag,
accessExpiresAt,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
}, {
new: true
});
return integrationAuth;
}

View File

@@ -170,7 +170,7 @@ export const updateSubscriptionOrgQuantity = async ({
); );
} }
await EELicenseService.refreshOrganizationPlan(organizationId); await EELicenseService.refreshPlan(organizationId);
return stripeSubscription; return stripeSubscription;
}; };

View File

@@ -1,16 +1,16 @@
import rateLimit from 'express-rate-limit'; import rateLimit from 'express-rate-limit';
const MongoStore = require('rate-limit-mongo'); // const MongoStore = require('rate-limit-mongo');
// 200 per minute // 200 per minute
export const apiLimiter = rateLimit({ export const apiLimiter = rateLimit({
store: new MongoStore({ // store: new MongoStore({
uri: process.env.MONGO_URL, // uri: process.env.MONGO_URL,
expireTimeMs: 1000 * 60, // expireTimeMs: 1000 * 60,
collectionName: "expressRateRecords-apiLimiter", // collectionName: "expressRateRecords-apiLimiter",
errorHandler: console.error.bind(null, 'rate-limit-mongo') // errorHandler: console.error.bind(null, 'rate-limit-mongo')
}), // }),
windowMs: 1000 * 60, windowMs: 60 * 1000,
max: 200, max: 240,
standardHeaders: true, standardHeaders: true,
legacyHeaders: false, legacyHeaders: false,
skip: (request) => { skip: (request) => {
@@ -23,14 +23,14 @@ export const apiLimiter = rateLimit({
// 50 requests per 1 hours // 50 requests per 1 hours
const authLimit = rateLimit({ const authLimit = rateLimit({
store: new MongoStore({ // store: new MongoStore({
uri: process.env.MONGO_URL, // uri: process.env.MONGO_URL,
expireTimeMs: 1000 * 60 * 60, // expireTimeMs: 1000 * 60 * 60,
errorHandler: console.error.bind(null, 'rate-limit-mongo'), // errorHandler: console.error.bind(null, 'rate-limit-mongo'),
collectionName: "expressRateRecords-authLimit", // collectionName: "expressRateRecords-authLimit",
}), // }),
windowMs: 1000 * 60 * 60, windowMs: 60 * 1000,
max: 50, max: 10,
standardHeaders: true, standardHeaders: true,
legacyHeaders: false, legacyHeaders: false,
keyGenerator: (req, res) => { keyGenerator: (req, res) => {
@@ -40,14 +40,14 @@ const authLimit = rateLimit({
// 5 requests per 1 hour // 5 requests per 1 hour
export const passwordLimiter = rateLimit({ export const passwordLimiter = rateLimit({
store: new MongoStore({ // store: new MongoStore({
uri: process.env.MONGO_URL, // uri: process.env.MONGO_URL,
expireTimeMs: 1000 * 60 * 60, // expireTimeMs: 1000 * 60 * 60,
errorHandler: console.error.bind(null, 'rate-limit-mongo'), // errorHandler: console.error.bind(null, 'rate-limit-mongo'),
collectionName: "expressRateRecords-passwordLimiter", // collectionName: "expressRateRecords-passwordLimiter",
}), // }),
windowMs: 1000 * 60 * 60, windowMs: 60 * 60 * 1000,
max: 5, max: 10,
standardHeaders: true, standardHeaders: true,
legacyHeaders: false, legacyHeaders: false,
keyGenerator: (req, res) => { keyGenerator: (req, res) => {

View File

@@ -57,7 +57,7 @@ import { getFolderIdFromServiceToken } from "../services/FolderService";
export const repackageSecretToRaw = ({ export const repackageSecretToRaw = ({
secret, secret,
key key
}:{ }: {
secret: ISecret; secret: ISecret;
key: string; key: string;
}) => { }) => {
@@ -76,8 +76,8 @@ export const repackageSecretToRaw = ({
key key
}); });
let secretComment: string = ''; let secretComment: string = '';
if (secret.secretCommentCiphertext && secret.secretCommentIV && secret.secretCommentTag) { if (secret.secretCommentCiphertext && secret.secretCommentIV && secret.secretCommentTag) {
secretComment = decryptSymmetric128BitHexKeyUTF8({ secretComment = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretCommentCiphertext, ciphertext: secret.secretCommentCiphertext,
@@ -86,7 +86,7 @@ export const repackageSecretToRaw = ({
key key
}); });
} }
return ({ return ({
_id: secret._id, _id: secret._id,
version: secret.version, version: secret.version,
@@ -503,7 +503,7 @@ export const getSecretsHelper = async ({
folder: folderId, folder: folderId,
type: SECRET_PERSONAL, type: SECRET_PERSONAL,
...getAuthDataPayloadUserObj(authData), ...getAuthDataPayloadUserObj(authData),
}).lean(); }).populate("tags").lean();
// concat with shared secrets // concat with shared secrets
secrets = secrets.concat( secrets = secrets.concat(
@@ -515,7 +515,7 @@ export const getSecretsHelper = async ({
secretBlindIndex: { secretBlindIndex: {
$nin: secrets.map((secret) => secret.secretBlindIndex), $nin: secrets.map((secret) => secret.secretBlindIndex),
}, },
}).lean() }).populate("tags").lean()
); );
// (EE) create (audit) log // (EE) create (audit) log
@@ -553,7 +553,7 @@ export const getSecretsHelper = async ({
}, },
}); });
} }
return secrets; return secrets;
}; };
@@ -652,7 +652,7 @@ export const getSecretHelper = async ({
}, },
}); });
} }
return secret; return secret;
}; };
@@ -843,7 +843,7 @@ export const deleteSecretHelper = async ({
// if using service token filter towards the folderId by secretpath // if using service token filter towards the folderId by secretpath
if (authData.authPayload instanceof ServiceTokenData) { if (authData.authPayload instanceof ServiceTokenData) {
const { secretPath: serviceTkScopedSecretPath } = authData.authPayload; const { secretPath: serviceTkScopedSecretPath } = authData.authPayload;
if (secretPath !== serviceTkScopedSecretPath) { if (secretPath !== serviceTkScopedSecretPath) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
} }
@@ -909,12 +909,12 @@ export const deleteSecretHelper = async ({
}); });
action && (await EELogService.createLog({ action && (await EELogService.createLog({
...getAuthDataPayloadIdObj(authData), ...getAuthDataPayloadIdObj(authData),
workspaceId, workspaceId,
actions: [action], actions: [action],
channel: authData.authChannel, channel: authData.authChannel,
ipAddress: authData.authIP, ipAddress: authData.authIP,
})); }));
// (EE) take a secret snapshot // (EE) take a secret snapshot
await EESecretService.takeSecretSnapshot({ await EESecretService.takeSecretSnapshot({
@@ -941,7 +941,7 @@ export const deleteSecretHelper = async ({
}, },
}); });
} }
return ({ return ({
secrets, secrets,
secret secret

View File

@@ -41,7 +41,7 @@ export const createWorkspace = async ({
workspaceId: workspace._id workspaceId: workspace._id
}); });
await EELicenseService.refreshOrganizationPlan(organizationId); await EELicenseService.refreshPlan(organizationId);
return workspace; return workspace;
}; };

View File

@@ -50,10 +50,12 @@ interface App {
const getApps = async ({ const getApps = async ({
integrationAuth, integrationAuth,
accessToken, accessToken,
accessId,
teamId, teamId,
}: { }: {
integrationAuth: IIntegrationAuth; integrationAuth: IIntegrationAuth;
accessToken: string; accessToken: string;
accessId?: string;
teamId?: string; teamId?: string;
}) => { }) => {
let apps: App[] = []; let apps: App[] = [];

View File

@@ -23,7 +23,7 @@ const requireIntegrationAuthorizationAuth = ({
return async (req: Request, res: Response, next: NextFunction) => { return async (req: Request, res: Response, next: NextFunction) => {
const { integrationAuthId } = req[location]; const { integrationAuthId } = req[location];
const { integrationAuth, accessToken } = await validateClientForIntegrationAuth({ const { integrationAuth, accessToken, accessId } = await validateClientForIntegrationAuth({
authData: req.authData, authData: req.authData,
integrationAuthId: new Types.ObjectId(integrationAuthId), integrationAuthId: new Types.ObjectId(integrationAuthId),
acceptedRoles, acceptedRoles,
@@ -38,6 +38,10 @@ const requireIntegrationAuthorizationAuth = ({
req.accessToken = accessToken; req.accessToken = accessToken;
} }
if (accessId) {
req.accessId = accessId;
}
return next(); return next();
}; };
}; };

View File

@@ -15,7 +15,7 @@ import {
INTEGRATION_TRAVISCI, INTEGRATION_TRAVISCI,
INTEGRATION_SUPABASE, INTEGRATION_SUPABASE,
INTEGRATION_CHECKLY, INTEGRATION_CHECKLY,
INTEGRATION_HASHICORP_VAULT INTEGRATION_HASHICORP_VAULT,
} from "../variables"; } from "../variables";
export interface IIntegration { export interface IIntegration {
@@ -33,23 +33,24 @@ export interface IIntegration {
targetServiceId: string; targetServiceId: string;
path: string; path: string;
region: string; region: string;
secretPath: string;
integration: integration:
| 'azure-key-vault' | "azure-key-vault"
| 'aws-parameter-store' | "aws-parameter-store"
| 'aws-secret-manager' | "aws-secret-manager"
| 'heroku' | "heroku"
| 'vercel' | "vercel"
| 'netlify' | "netlify"
| 'github' | "github"
| 'gitlab' | "gitlab"
| 'render' | "render"
| 'railway' | "railway"
| 'flyio' | "flyio"
| 'circleci' | "circleci"
| 'travisci' | "travisci"
| 'supabase' | "supabase"
| 'checkly' | "checkly"
| 'hashicorp-vault'; | "hashicorp-vault";
integrationAuth: Types.ObjectId; integrationAuth: Types.ObjectId;
} }
@@ -71,7 +72,7 @@ const integrationSchema = new Schema<IIntegration>(
url: { url: {
// for custom self-hosted integrations (e.g. self-hosted GitHub enterprise) // for custom self-hosted integrations (e.g. self-hosted GitHub enterprise)
type: String, type: String,
default: null default: null,
}, },
app: { app: {
// name of app in provider // name of app in provider
@@ -90,17 +91,17 @@ const integrationSchema = new Schema<IIntegration>(
}, },
targetEnvironmentId: { targetEnvironmentId: {
type: String, type: String,
default: null default: null,
}, },
targetService: { targetService: {
// railway-specific service // railway-specific service
type: String, type: String,
default: null default: null,
}, },
targetServiceId: { targetServiceId: {
// railway-specific service // railway-specific service
type: String, type: String,
default: null default: null,
}, },
owner: { owner: {
// github-specific repo owner-login // github-specific repo owner-login
@@ -111,12 +112,12 @@ const integrationSchema = new Schema<IIntegration>(
// aws-parameter-store-specific path // aws-parameter-store-specific path
// (also) vercel preview-branch // (also) vercel preview-branch
type: String, type: String,
default: null default: null,
}, },
region: { region: {
// aws-parameter-store-specific path // aws-parameter-store-specific path
type: String, type: String,
default: null default: null,
}, },
integration: { integration: {
type: String, type: String,
@@ -136,7 +137,7 @@ const integrationSchema = new Schema<IIntegration>(
INTEGRATION_TRAVISCI, INTEGRATION_TRAVISCI,
INTEGRATION_SUPABASE, INTEGRATION_SUPABASE,
INTEGRATION_CHECKLY, INTEGRATION_CHECKLY,
INTEGRATION_HASHICORP_VAULT INTEGRATION_HASHICORP_VAULT,
], ],
required: true, required: true,
}, },
@@ -145,6 +146,11 @@ const integrationSchema = new Schema<IIntegration>(
ref: "IntegrationAuth", ref: "IntegrationAuth",
required: true, required: true,
}, },
secretPath: {
type: String,
required: true,
default: "/",
},
}, },
{ {
timestamps: true, timestamps: true,

View File

@@ -1,75 +1,77 @@
import express from 'express'; import express from "express";
const router = express.Router(); const router = express.Router();
import { import {
requireAuth, requireAuth,
requireIntegrationAuth, requireIntegrationAuth,
requireIntegrationAuthorizationAuth, requireIntegrationAuthorizationAuth,
validateRequest validateRequest,
} from '../../middleware'; } from "../../middleware";
import { import {
ADMIN, ADMIN,
MEMBER, MEMBER,
AUTH_MODE_JWT, AUTH_MODE_JWT,
AUTH_MODE_API_KEY AUTH_MODE_API_KEY,
} from '../../variables'; } from "../../variables";
import { body, param } from 'express-validator'; import { body, param } from "express-validator";
import { integrationController } from '../../controllers/v1'; import { integrationController } from "../../controllers/v1";
router.post( router.post(
'/', "/",
requireAuth({ requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY] acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY],
}), }),
requireIntegrationAuthorizationAuth({ requireIntegrationAuthorizationAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
location: 'body' location: "body",
}), }),
body('integrationAuthId').exists().isString().trim(), body("integrationAuthId").exists().isString().trim(),
body('app').trim(), body("app").trim(),
body('isActive').exists().isBoolean(), body("isActive").exists().isBoolean(),
body('appId').trim(), body("appId").trim(),
body('sourceEnvironment').trim(), body("secretPath").default("/").isString().trim(),
body('targetEnvironment').trim(), body("sourceEnvironment").trim(),
body('targetEnvironmentId').trim(), body("targetEnvironment").trim(),
body('targetService').trim(), body("targetEnvironmentId").trim(),
body('targetServiceId').trim(), body("targetService").trim(),
body('owner').trim(), body("targetServiceId").trim(),
body('path').trim(), body("owner").trim(),
body('region').trim(), body("path").trim(),
validateRequest, body("region").trim(),
integrationController.createIntegration validateRequest,
integrationController.createIntegration
); );
router.patch( router.patch(
'/:integrationId', "/:integrationId",
requireAuth({ requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT] acceptedAuthModes: [AUTH_MODE_JWT],
}), }),
requireIntegrationAuth({ requireIntegrationAuth({
acceptedRoles: [ADMIN, MEMBER] acceptedRoles: [ADMIN, MEMBER],
}), }),
param('integrationId').exists().trim(), param("integrationId").exists().trim(),
body('isActive').exists().isBoolean(), body("isActive").exists().isBoolean(),
body('app').exists().trim(), body("app").exists().trim(),
body('environment').exists().trim(), body("secretPath").default("/").isString().trim(),
body('appId').exists(), body("environment").exists().trim(),
body('targetEnvironment').exists(), body("appId").exists(),
body('owner').exists(), body("targetEnvironment").exists(),
validateRequest, body("owner").exists(),
integrationController.updateIntegration validateRequest,
integrationController.updateIntegration
); );
router.delete( router.delete(
'/:integrationId', "/:integrationId",
requireAuth({ requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT] acceptedAuthModes: [AUTH_MODE_JWT],
}), }),
requireIntegrationAuth({ requireIntegrationAuth({
acceptedRoles: [ADMIN, MEMBER] acceptedRoles: [ADMIN, MEMBER],
}), }),
param('integrationId').exists().trim(), param("integrationId").exists().trim(),
validateRequest, validateRequest,
integrationController.deleteIntegration integrationController.deleteIntegration
); );
export default router; export default router;

View File

@@ -63,6 +63,7 @@ router.get(
query("workspaceId").exists().isString().trim(), query("workspaceId").exists().isString().trim(),
query("environment").exists().isString().trim(), query("environment").exists().isString().trim(),
query("parentFolderId").optional().isString().trim(), query("parentFolderId").optional().isString().trim(),
query("parentFolderPath").optional().isString().trim(),
validateRequest, validateRequest,
getFolders getFolders
); );

View File

@@ -23,7 +23,6 @@ import {
router.get( router.get(
"/raw", "/raw",
query("workspaceId").exists().isString().trim(), query("workspaceId").exists().isString().trim(),
query("workspaceId").exists().isString().trim(),
query("environment").exists().isString().trim(), query("environment").exists().isString().trim(),
query("secretPath").default("/").isString().trim(), query("secretPath").default("/").isString().trim(),
validateRequest, validateRequest,

View File

@@ -1,110 +1,112 @@
import { Types } from 'mongoose'; import { Types } from "mongoose";
import { import {
getSecretsBotHelper, getSecretsBotHelper,
encryptSymmetricHelper, encryptSymmetricHelper,
decryptSymmetricHelper, decryptSymmetricHelper,
getKey, getKey,
getIsWorkspaceE2EEHelper getIsWorkspaceE2EEHelper,
} from '../helpers/bot'; } from "../helpers/bot";
/** /**
* Class to handle bot actions * Class to handle bot actions
*/ */
class BotService { class BotService {
/**
/** * Return whether or not workspace with id [workspaceId] is end-to-end encrypted
* Return whether or not workspace with id [workspaceId] is end-to-end encrypted * @param workspaceId - id of workspace
* @param workspaceId - id of workspace * @returns {Boolean}
* @returns {Boolean} */
*/ static async getIsWorkspaceE2EE(workspaceId: Types.ObjectId) {
static async getIsWorkspaceE2EE(workspaceId: Types.ObjectId) { return await getIsWorkspaceE2EEHelper(workspaceId);
return await getIsWorkspaceE2EEHelper(workspaceId); }
}
/** /**
* Get workspace key for workspace with id [workspaceId] shared to bot. * Get workspace key for workspace with id [workspaceId] shared to bot.
* @param {Object} obj * @param {Object} obj
* @param {Types.ObjectId} obj.workspaceId - id of workspace to get workspace key for * @param {Types.ObjectId} obj.workspaceId - id of workspace to get workspace key for
* @returns * @returns
*/ */
static async getWorkspaceKeyWithBot({ static async getWorkspaceKeyWithBot({
workspaceId workspaceId,
}: { }: {
workspaceId: Types.ObjectId; workspaceId: Types.ObjectId;
}) { }) {
return await getKey({ return await getKey({
workspaceId workspaceId,
}); });
} }
/** /**
* Return decrypted secrets for workspace with id [workspaceId] and * Return decrypted secrets for workspace with id [workspaceId] and
* environment [environmen] shared to bot. * environment [environmen] shared to bot.
* @param {Object} obj * @param {Object} obj
* @param {String} obj.workspaceId - id of workspace of secrets * @param {String} obj.workspaceId - id of workspace of secrets
* @param {String} obj.environment - environment for secrets * @param {String} obj.environment - environment for secrets
* @returns {Object} secretObj - object where keys are secret keys and values are secret values * @returns {Object} secretObj - object where keys are secret keys and values are secret values
*/ */
static async getSecrets({ static async getSecrets({
workspaceId, workspaceId,
environment environment,
}: { secretPath,
workspaceId: Types.ObjectId; }: {
environment: string; workspaceId: Types.ObjectId;
}) { environment: string;
return await getSecretsBotHelper({ secretPath: string;
workspaceId, }) {
environment return await getSecretsBotHelper({
}); workspaceId,
} environment,
secretPath,
/** });
* Return symmetrically encrypted [plaintext] using the }
* bot's copy of the workspace key for workspace with id [workspaceId]
* @param {Object} obj /**
* @param {String} obj.workspaceId - id of workspace * Return symmetrically encrypted [plaintext] using the
* @param {String} obj.plaintext - plaintext to encrypt * bot's copy of the workspace key for workspace with id [workspaceId]
*/ * @param {Object} obj
static async encryptSymmetric({ * @param {String} obj.workspaceId - id of workspace
workspaceId, * @param {String} obj.plaintext - plaintext to encrypt
plaintext */
}: { static async encryptSymmetric({
workspaceId: Types.ObjectId; workspaceId,
plaintext: string; plaintext,
}) { }: {
return await encryptSymmetricHelper({ workspaceId: Types.ObjectId;
workspaceId, plaintext: string;
plaintext }) {
}); return await encryptSymmetricHelper({
} workspaceId,
plaintext,
/** });
* Return symmetrically decrypted [ciphertext] using the }
* bot's copy of the workspace key for workspace with id [workspaceId]
* @param {Object} obj /**
* @param {String} obj.workspaceId - id of workspace * Return symmetrically decrypted [ciphertext] using the
* @param {String} obj.ciphertext - ciphertext to decrypt * bot's copy of the workspace key for workspace with id [workspaceId]
* @param {String} obj.iv - iv * @param {Object} obj
* @param {String} obj.tag - tag * @param {String} obj.workspaceId - id of workspace
*/ * @param {String} obj.ciphertext - ciphertext to decrypt
static async decryptSymmetric({ * @param {String} obj.iv - iv
workspaceId, * @param {String} obj.tag - tag
ciphertext, */
iv, static async decryptSymmetric({
tag workspaceId,
}: { ciphertext,
workspaceId: Types.ObjectId; iv,
ciphertext: string; tag,
iv: string; }: {
tag: string; workspaceId: Types.ObjectId;
}) { ciphertext: string;
return await decryptSymmetricHelper({ iv: string;
workspaceId, tag: string;
ciphertext, }) {
iv, return await decryptSymmetricHelper({
tag workspaceId,
}); ciphertext,
} iv,
tag,
});
}
} }
export default BotService; export default BotService;

View File

@@ -37,6 +37,7 @@ declare global {
serviceToken: any; serviceToken: any;
serviceAccount: any; serviceAccount: any;
accessToken: any; accessToken: any;
accessId: any;
serviceTokenData: any; serviceTokenData: any;
apiKeyData: any; apiKeyData: any;
query?: any; query?: any;

View File

@@ -13,6 +13,7 @@ import {
BackupPrivateKey, BackupPrivateKey,
IntegrationAuth, IntegrationAuth,
ServiceTokenData, ServiceTokenData,
Integration,
} from "../../models"; } from "../../models";
import { generateKeyPair } from "../../utils/crypto"; import { generateKeyPair } from "../../utils/crypto";
import { client, getEncryptionKey, getRootEncryptionKey } from "../../config"; import { client, getEncryptionKey, getRootEncryptionKey } from "../../config";
@@ -424,3 +425,19 @@ export const backfillServiceToken = async () => {
); );
console.log("Migration: Service token migration v1 complete"); console.log("Migration: Service token migration v1 complete");
}; };
export const backfillIntegration = async () => {
await Integration.updateMany(
{
secretPath: {
$exists: false,
},
},
{
$set: {
secretPath: "/",
},
}
);
console.log("Migration: Integration migration v1 complete");
};

View File

@@ -13,6 +13,7 @@ import {
backfillEncryptionMetadata, backfillEncryptionMetadata,
backfillSecretFolders, backfillSecretFolders,
backfillServiceToken, backfillServiceToken,
backfillIntegration,
} from "./backfillData"; } from "./backfillData";
import { import {
reencryptBotPrivateKeys, reencryptBotPrivateKeys,
@@ -77,6 +78,7 @@ export const setup = async () => {
await backfillEncryptionMetadata(); await backfillEncryptionMetadata();
await backfillSecretFolders(); await backfillSecretFolders();
await backfillServiceToken(); await backfillServiceToken();
await backfillIntegration();
// re-encrypt any data previously encrypted under server hex 128-bit ENCRYPTION_KEY // re-encrypt any data previously encrypted under server hex 128-bit ENCRYPTION_KEY
// to base64 256-bit ROOT_ENCRYPTION_KEY // to base64 256-bit ROOT_ENCRYPTION_KEY

View File

@@ -56,11 +56,14 @@ import { validateServiceAccountClientForWorkspace } from './serviceAccount';
if (!integrationAuth) throw IntegrationAuthNotFoundError(); if (!integrationAuth) throw IntegrationAuthNotFoundError();
let accessToken; let accessToken, accessId;
if (attachAccessToken) { if (attachAccessToken) {
accessToken = (await IntegrationService.getIntegrationAuthAccess({ const access = (await IntegrationService.getIntegrationAuthAccess({
integrationAuthId: integrationAuth._id integrationAuthId: integrationAuth._id
})).accessToken; }));
accessToken = access.accessToken;
accessId = access.accessId;
} }
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) { if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
@@ -70,7 +73,7 @@ import { validateServiceAccountClientForWorkspace } from './serviceAccount';
acceptedRoles acceptedRoles
}); });
return ({ integrationAuth, accessToken }); return ({ integrationAuth, accessToken, accessId });
} }
if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) { if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) {
@@ -79,7 +82,7 @@ import { validateServiceAccountClientForWorkspace } from './serviceAccount';
workspaceId: integrationAuth.workspace._id workspaceId: integrationAuth.workspace._id
}); });
return ({ integrationAuth, accessToken }); return ({ integrationAuth, accessToken, accessId });
} }
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) { if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) {
@@ -95,7 +98,7 @@ import { validateServiceAccountClientForWorkspace } from './serviceAccount';
acceptedRoles acceptedRoles
}); });
return ({ integrationAuth, accessToken }); return ({ integrationAuth, accessToken, accessId });
} }
throw UnauthorizedRequestError({ throw UnauthorizedRequestError({

View File

@@ -2,6 +2,7 @@ package api
import ( import (
"fmt" "fmt"
"net/http"
"github.com/Infisical/infisical-merge/packages/config" "github.com/Infisical/infisical-merge/packages/config"
"github.com/go-resty/resty/v2" "github.com/go-resty/resty/v2"
@@ -10,63 +11,6 @@ import (
const USER_AGENT = "cli" const USER_AGENT = "cli"
func CallBatchModifySecretsByWorkspaceAndEnv(httpClient *resty.Client, request BatchModifySecretsByWorkspaceAndEnvRequest) error {
endpoint := fmt.Sprintf("%v/v2/secrets", config.INFISICAL_URL)
response, err := httpClient.
R().
SetBody(request).
SetHeader("User-Agent", USER_AGENT).
Patch(endpoint)
if err != nil {
return fmt.Errorf("CallBatchModifySecretsByWorkspaceAndEnv: Unable to complete api request [err=%s]", err)
}
if response.IsError() {
return fmt.Errorf("CallBatchModifySecretsByWorkspaceAndEnv: Unsuccessful response: [response=%s]", response)
}
return nil
}
func CallBatchCreateSecretsByWorkspaceAndEnv(httpClient *resty.Client, request BatchCreateSecretsByWorkspaceAndEnvRequest) error {
endpoint := fmt.Sprintf("%v/v2/secrets/", config.INFISICAL_URL)
response, err := httpClient.
R().
SetBody(request).
SetHeader("User-Agent", USER_AGENT).
Post(endpoint)
if err != nil {
return fmt.Errorf("CallBatchCreateSecretsByWorkspaceAndEnv: Unable to complete api request [err=%s]", err)
}
if response.IsError() {
return fmt.Errorf("CallBatchCreateSecretsByWorkspaceAndEnv: Unsuccessful response: [response=%s]", response)
}
return nil
}
func CallBatchDeleteSecretsByWorkspaceAndEnv(httpClient *resty.Client, request BatchDeleteSecretsBySecretIdsRequest) error {
endpoint := fmt.Sprintf("%v/v2/secrets", config.INFISICAL_URL)
response, err := httpClient.
R().
SetBody(request).
SetHeader("User-Agent", USER_AGENT).
Delete(endpoint)
if err != nil {
return fmt.Errorf("CallBatchDeleteSecretsByWorkspaceAndEnv: Unable to complete api request [err=%s]", err)
}
if response.IsError() {
return fmt.Errorf("CallBatchDeleteSecretsByWorkspaceAndEnv: Unsuccessful response: [response=%s]", response)
}
return nil
}
func CallGetEncryptedWorkspaceKey(httpClient *resty.Client, request GetEncryptedWorkspaceKeyRequest) (GetEncryptedWorkspaceKeyResponse, error) { func CallGetEncryptedWorkspaceKey(httpClient *resty.Client, request GetEncryptedWorkspaceKeyRequest) (GetEncryptedWorkspaceKeyResponse, error) {
endpoint := fmt.Sprintf("%v/v2/workspace/%v/encrypted-key", config.INFISICAL_URL, request.WorkspaceId) endpoint := fmt.Sprintf("%v/v2/workspace/%v/encrypted-key", config.INFISICAL_URL, request.WorkspaceId)
var result GetEncryptedWorkspaceKeyResponse var result GetEncryptedWorkspaceKeyResponse
@@ -106,28 +50,6 @@ func CallGetServiceTokenDetailsV2(httpClient *resty.Client) (GetServiceTokenDeta
return tokenDetailsResponse, nil return tokenDetailsResponse, nil
} }
func CallGetSecretsV2(httpClient *resty.Client, request GetEncryptedSecretsV2Request) (GetEncryptedSecretsV2Response, error) {
var secretsResponse GetEncryptedSecretsV2Response
response, err := httpClient.
R().
SetResult(&secretsResponse).
SetHeader("User-Agent", USER_AGENT).
SetQueryParam("environment", request.Environment).
SetQueryParam("workspaceId", request.WorkspaceId).
SetQueryParam("tagSlugs", request.TagSlugs).
Get(fmt.Sprintf("%v/v2/secrets", config.INFISICAL_URL))
if err != nil {
return GetEncryptedSecretsV2Response{}, fmt.Errorf("CallGetSecretsV2: Unable to complete api request [err=%s]", err)
}
if response.IsError() {
return GetEncryptedSecretsV2Response{}, fmt.Errorf("CallGetSecretsV2: Unsuccessful response: [response=%s]", response)
}
return secretsResponse, nil
}
func CallLogin1V2(httpClient *resty.Client, request GetLoginOneV2Request) (GetLoginOneV2Response, error) { func CallLogin1V2(httpClient *resty.Client, request GetLoginOneV2Request) (GetLoginOneV2Response, error) {
var loginOneV2Response GetLoginOneV2Response var loginOneV2Response GetLoginOneV2Response
response, err := httpClient. response, err := httpClient.
@@ -159,6 +81,22 @@ func CallVerifyMfaToken(httpClient *resty.Client, request VerifyMfaTokenRequest)
SetBody(request). SetBody(request).
Post(fmt.Sprintf("%v/v2/auth/mfa/verify", config.INFISICAL_URL)) Post(fmt.Sprintf("%v/v2/auth/mfa/verify", config.INFISICAL_URL))
cookies := response.Cookies()
// Find a cookie by name
cookieName := "jid"
var refreshToken *http.Cookie
for _, cookie := range cookies {
if cookie.Name == cookieName {
refreshToken = cookie
break
}
}
// When MFA is enabled
if refreshToken != nil {
verifyMfaTokenResponse.RefreshToken = refreshToken.Value
}
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("CallVerifyMfaToken: Unable to complete api request [err=%s]", err) return nil, nil, fmt.Errorf("CallVerifyMfaToken: Unable to complete api request [err=%s]", err)
} }
@@ -179,6 +117,22 @@ func CallLogin2V2(httpClient *resty.Client, request GetLoginTwoV2Request) (GetLo
SetBody(request). SetBody(request).
Post(fmt.Sprintf("%v/v2/auth/login2", config.INFISICAL_URL)) Post(fmt.Sprintf("%v/v2/auth/login2", config.INFISICAL_URL))
cookies := response.Cookies()
// Find a cookie by name
cookieName := "jid"
var refreshToken *http.Cookie
for _, cookie := range cookies {
if cookie.Name == cookieName {
refreshToken = cookie
break
}
}
// When MFA is enabled
if refreshToken != nil {
loginTwoV2Response.RefreshToken = refreshToken.Value
}
if err != nil { if err != nil {
return GetLoginTwoV2Response{}, fmt.Errorf("CallLogin2V2: Unable to complete api request [err=%s]", err) return GetLoginTwoV2Response{}, fmt.Errorf("CallLogin2V2: Unable to complete api request [err=%s]", err)
} }
@@ -247,3 +201,133 @@ func CallGetAccessibleEnvironments(httpClient *resty.Client, request GetAccessib
return accessibleEnvironmentsResponse, nil return accessibleEnvironmentsResponse, nil
} }
func CallGetNewAccessTokenWithRefreshToken(httpClient *resty.Client, refreshToken string) (GetNewAccessTokenWithRefreshTokenResponse, error) {
var newAccessToken GetNewAccessTokenWithRefreshTokenResponse
response, err := httpClient.
R().
SetResult(&newAccessToken).
SetHeader("User-Agent", USER_AGENT).
SetCookie(&http.Cookie{
Name: "jid",
Value: refreshToken,
}).
Post(fmt.Sprintf("%v/v1/auth/token", config.INFISICAL_URL))
if err != nil {
return GetNewAccessTokenWithRefreshTokenResponse{}, err
}
if response.IsError() {
return GetNewAccessTokenWithRefreshTokenResponse{}, fmt.Errorf("CallGetNewAccessTokenWithRefreshToken: Unsuccessful response: [response=%v]", response)
}
return newAccessToken, nil
}
func CallGetSecretsV3(httpClient *resty.Client, request GetEncryptedSecretsV3Request) (GetEncryptedSecretsV3Response, error) {
var secretsResponse GetEncryptedSecretsV3Response
httpRequest := httpClient.
R().
SetResult(&secretsResponse).
SetHeader("User-Agent", USER_AGENT).
SetQueryParam("environment", request.Environment).
SetQueryParam("workspaceId", request.WorkspaceId)
if request.SecretPath != "" {
httpRequest.SetQueryParam("secretPath", request.SecretPath)
}
response, err := httpRequest.Get(fmt.Sprintf("%v/v3/secrets", config.INFISICAL_URL))
if err != nil {
return GetEncryptedSecretsV3Response{}, fmt.Errorf("CallGetSecretsV3: Unable to complete api request [err=%s]", err)
}
if response.IsError() {
return GetEncryptedSecretsV3Response{}, fmt.Errorf("CallGetSecretsV3: Unsuccessful response. Please make sure your secret path, workspace and environment name are all correct [response=%s]", response)
}
return secretsResponse, nil
}
func CallCreateSecretsV3(httpClient *resty.Client, request CreateSecretV3Request) error {
var secretsResponse GetEncryptedSecretsV3Response
response, err := httpClient.
R().
SetResult(&secretsResponse).
SetHeader("User-Agent", USER_AGENT).
SetBody(request).
Post(fmt.Sprintf("%v/v3/secrets/%s", config.INFISICAL_URL, request.SecretName))
if err != nil {
return fmt.Errorf("CallCreateSecretsV3: Unable to complete api request [err=%s]", err)
}
if response.IsError() {
return fmt.Errorf("CallCreateSecretsV3: Unsuccessful response. Please make sure your secret path, workspace and environment name are all correct [response=%s]", response)
}
return nil
}
func CallDeleteSecretsV3(httpClient *resty.Client, request DeleteSecretV3Request) error {
var secretsResponse GetEncryptedSecretsV3Response
response, err := httpClient.
R().
SetResult(&secretsResponse).
SetHeader("User-Agent", USER_AGENT).
SetBody(request).
Delete(fmt.Sprintf("%v/v3/secrets/%s", config.INFISICAL_URL, request.SecretName))
if err != nil {
return fmt.Errorf("CallDeleteSecretsV3: Unable to complete api request [err=%s]", err)
}
if response.IsError() {
return fmt.Errorf("CallDeleteSecretsV3: Unsuccessful response. Please make sure your secret path, workspace and environment name are all correct [response=%s]", response)
}
return nil
}
func CallUpdateSecretsV3(httpClient *resty.Client, request UpdateSecretByNameV3Request) error {
var secretsResponse GetEncryptedSecretsV3Response
response, err := httpClient.
R().
SetResult(&secretsResponse).
SetHeader("User-Agent", USER_AGENT).
SetBody(request).
Patch(fmt.Sprintf("%v/v3/secrets/%s", config.INFISICAL_URL, request.SecretName))
if err != nil {
return fmt.Errorf("CallUpdateSecretsV3: Unable to complete api request [err=%s]", err)
}
if response.IsError() {
return fmt.Errorf("CallUpdateSecretsV3: Unsuccessful response. Please make sure your secret path, workspace and environment name are all correct [response=%s]", response)
}
return nil
}
func CallGetSingleSecretByNameV3(httpClient *resty.Client, request CreateSecretV3Request) error {
var secretsResponse GetEncryptedSecretsV3Response
response, err := httpClient.
R().
SetResult(&secretsResponse).
SetHeader("User-Agent", USER_AGENT).
SetBody(request).
Post(fmt.Sprintf("%v/v3/secrets/%s", config.INFISICAL_URL, request.SecretName))
if err != nil {
return fmt.Errorf("CallGetSingleSecretByNameV3: Unable to complete api request [err=%s]", err)
}
if response.IsError() {
return fmt.Errorf("CallGetSingleSecretByNameV3: Unsuccessful response. Please make sure your secret path, workspace and environment name are all correct [response=%s]", response)
}
return nil
}

View File

@@ -143,24 +143,7 @@ type Secret struct {
SecretCommentHash string `json:"secretCommentHash,omitempty"` SecretCommentHash string `json:"secretCommentHash,omitempty"`
Type string `json:"type,omitempty"` Type string `json:"type,omitempty"`
ID string `json:"id,omitempty"` ID string `json:"id,omitempty"`
} PlainTextKey string `json:"plainTextKey"`
type BatchCreateSecretsByWorkspaceAndEnvRequest struct {
Environment string `json:"environment"`
WorkspaceId string `json:"workspaceId"`
Secrets []Secret `json:"secrets"`
}
type BatchModifySecretsByWorkspaceAndEnvRequest struct {
Environment string `json:"environment"`
WorkspaceId string `json:"workspaceId"`
Secrets []Secret `json:"secrets"`
}
type BatchDeleteSecretsBySecretIdsRequest struct {
EnvironmentName string `json:"environmentName"`
WorkspaceId string `json:"workspaceId"`
SecretIds []string `json:"secretIds"`
} }
type GetEncryptedWorkspaceKeyRequest struct { type GetEncryptedWorkspaceKeyRequest struct {
@@ -194,41 +177,6 @@ type GetSecretsByWorkspaceIdAndEnvironmentRequest struct {
WorkspaceId string `json:"workspaceId"` WorkspaceId string `json:"workspaceId"`
} }
type GetEncryptedSecretsV2Request struct {
Environment string `json:"environment"`
WorkspaceId string `json:"workspaceId"`
TagSlugs string `json:"tagSlugs"`
}
type GetEncryptedSecretsV2Response struct {
Secrets []struct {
ID string `json:"_id"`
Version int `json:"version"`
Workspace string `json:"workspace"`
Type string `json:"type"`
Environment string `json:"environment"`
SecretKeyCiphertext string `json:"secretKeyCiphertext"`
SecretKeyIV string `json:"secretKeyIV"`
SecretKeyTag string `json:"secretKeyTag"`
SecretValueCiphertext string `json:"secretValueCiphertext"`
SecretValueIV string `json:"secretValueIV"`
SecretValueTag string `json:"secretValueTag"`
SecretCommentCiphertext string `json:"secretCommentCiphertext"`
SecretCommentIV string `json:"secretCommentIV"`
SecretCommentTag string `json:"secretCommentTag"`
V int `json:"__v"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
User string `json:"user,omitempty"`
Tags []struct {
ID string `json:"_id"`
Name string `json:"name"`
Slug string `json:"slug"`
Workspace string `json:"workspace"`
} `json:"tags"`
} `json:"secrets"`
}
type GetServiceTokenDetailsResponse struct { type GetServiceTokenDetailsResponse struct {
ID string `json:"_id"` ID string `json:"_id"`
Name string `json:"name"` Name string `json:"name"`
@@ -281,6 +229,7 @@ type GetLoginTwoV2Response struct {
ProtectedKey string `json:"protectedKey"` ProtectedKey string `json:"protectedKey"`
ProtectedKeyIV string `json:"protectedKeyIV"` ProtectedKeyIV string `json:"protectedKeyIV"`
ProtectedKeyTag string `json:"protectedKeyTag"` ProtectedKeyTag string `json:"protectedKeyTag"`
RefreshToken string `json:"RefreshToken"`
} }
type VerifyMfaTokenRequest struct { type VerifyMfaTokenRequest struct {
@@ -298,6 +247,7 @@ type VerifyMfaTokenResponse struct {
ProtectedKey string `json:"protectedKey"` ProtectedKey string `json:"protectedKey"`
ProtectedKeyIV string `json:"protectedKeyIV"` ProtectedKeyIV string `json:"protectedKeyIV"`
ProtectedKeyTag string `json:"protectedKeyTag"` ProtectedKeyTag string `json:"protectedKeyTag"`
RefreshToken string `json:"refreshToken"`
} }
type VerifyMfaTokenErrorResponse struct { type VerifyMfaTokenErrorResponse struct {
@@ -314,3 +264,113 @@ type VerifyMfaTokenErrorResponse struct {
Application string `json:"application"` Application string `json:"application"`
Extra []interface{} `json:"extra"` Extra []interface{} `json:"extra"`
} }
type GetNewAccessTokenWithRefreshTokenResponse struct {
Token string `json:"token"`
}
type GetEncryptedSecretsV3Request struct {
Environment string `json:"environment"`
WorkspaceId string `json:"workspaceId"`
SecretPath string `json:"secretPath"`
}
type GetEncryptedSecretsV3Response struct {
Secrets []struct {
ID string `json:"_id"`
Version int `json:"version"`
Workspace string `json:"workspace"`
Type string `json:"type"`
Tags []struct {
ID string `json:"_id"`
Name string `json:"name"`
Slug string `json:"slug"`
Workspace string `json:"workspace"`
} `json:"tags"`
Environment string `json:"environment"`
SecretKeyCiphertext string `json:"secretKeyCiphertext"`
SecretKeyIV string `json:"secretKeyIV"`
SecretKeyTag string `json:"secretKeyTag"`
SecretValueCiphertext string `json:"secretValueCiphertext"`
SecretValueIV string `json:"secretValueIV"`
SecretValueTag string `json:"secretValueTag"`
SecretCommentCiphertext string `json:"secretCommentCiphertext"`
SecretCommentIV string `json:"secretCommentIV"`
SecretCommentTag string `json:"secretCommentTag"`
Algorithm string `json:"algorithm"`
KeyEncoding string `json:"keyEncoding"`
Folder string `json:"folder"`
V int `json:"__v"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
} `json:"secrets"`
}
type CreateSecretV3Request struct {
SecretName string `json:"secretName"`
WorkspaceID string `json:"workspaceId"`
Type string `json:"type"`
Environment string `json:"environment"`
SecretKeyCiphertext string `json:"secretKeyCiphertext"`
SecretKeyIV string `json:"secretKeyIV"`
SecretKeyTag string `json:"secretKeyTag"`
SecretValueCiphertext string `json:"secretValueCiphertext"`
SecretValueIV string `json:"secretValueIV"`
SecretValueTag string `json:"secretValueTag"`
SecretCommentCiphertext string `json:"secretCommentCiphertext"`
SecretCommentIV string `json:"secretCommentIV"`
SecretCommentTag string `json:"secretCommentTag"`
SecretPath string `json:"secretPath"`
}
type DeleteSecretV3Request struct {
SecretName string `json:"secretName"`
WorkspaceId string `json:"workspaceId"`
Environment string `json:"environment"`
Type string `json:"type"`
SecretPath string `json:"secretPath"`
}
type UpdateSecretByNameV3Request struct {
SecretName string `json:"secretName"`
WorkspaceID string `json:"workspaceId"`
Environment string `json:"environment"`
Type string `json:"type"`
SecretPath string `json:"secretPath"`
SecretValueCiphertext string `json:"secretValueCiphertext"`
SecretValueIV string `json:"secretValueIV"`
SecretValueTag string `json:"secretValueTag"`
}
type GetSingleSecretByNameV3Request struct {
SecretName string `json:"secretName"`
WorkspaceId string `json:"workspaceId"`
Environment string `json:"environment"`
Type string `json:"type"`
SecretPath string `json:"secretPath"`
}
type GetSingleSecretByNameSecretResponse struct {
Secrets []struct {
ID string `json:"_id"`
Version int `json:"version"`
Workspace string `json:"workspace"`
Type string `json:"type"`
Environment string `json:"environment"`
SecretKeyCiphertext string `json:"secretKeyCiphertext"`
SecretKeyIV string `json:"secretKeyIV"`
SecretKeyTag string `json:"secretKeyTag"`
SecretValueCiphertext string `json:"secretValueCiphertext"`
SecretValueIV string `json:"secretValueIV"`
SecretValueTag string `json:"secretValueTag"`
SecretCommentCiphertext string `json:"secretCommentCiphertext"`
SecretCommentIV string `json:"secretCommentIV"`
SecretCommentTag string `json:"secretCommentTag"`
Algorithm string `json:"algorithm"`
KeyEncoding string `json:"keyEncoding"`
Folder string `json:"folder"`
V int `json:"__v"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
} `json:"secrets"`
}

View File

@@ -97,7 +97,7 @@ var loginCmd = &cobra.Command{
loginOneResponse, loginTwoResponse, err := getFreshUserCredentials(email, password) loginOneResponse, loginTwoResponse, err := getFreshUserCredentials(email, password)
if err != nil { if err != nil {
fmt.Println("Unable to authenticate with the provided credentials, please try again") log.Warn().Msg("Unable to authenticate with the provided credentials, please ensure your email and password are correct")
log.Debug().Err(err) log.Debug().Err(err)
return return
} }
@@ -143,7 +143,7 @@ var loginCmd = &cobra.Command{
loginTwoResponse.Tag = verifyMFAresponse.Tag loginTwoResponse.Tag = verifyMFAresponse.Tag
loginTwoResponse.Token = verifyMFAresponse.Token loginTwoResponse.Token = verifyMFAresponse.Token
loginTwoResponse.EncryptionVersion = verifyMFAresponse.EncryptionVersion loginTwoResponse.EncryptionVersion = verifyMFAresponse.EncryptionVersion
loginTwoResponse.RefreshToken = verifyMFAresponse.RefreshToken
break break
} }
} }
@@ -244,9 +244,10 @@ var loginCmd = &cobra.Command{
} }
userCredentialsToBeStored := &models.UserCredentials{ userCredentialsToBeStored := &models.UserCredentials{
Email: email, Email: email,
PrivateKey: string(decryptedPrivateKey), PrivateKey: string(decryptedPrivateKey),
JTWToken: loginTwoResponse.Token, JTWToken: loginTwoResponse.Token,
RefreshToken: loginTwoResponse.RefreshToken,
} }
err = util.StoreUserCredsInKeyRing(userCredentialsToBeStored) err = util.StoreUserCredsInKeyRing(userCredentialsToBeStored)
@@ -414,7 +415,7 @@ func getFreshUserCredentials(email string, password string) (*api.GetLoginOneV2R
}) })
if err != nil { if err != nil {
util.HandleError(err) return nil, nil, err
} }
// **** Login 2 // **** Login 2

View File

@@ -82,7 +82,12 @@ var runCmd = &cobra.Command{
util.HandleError(err, "Unable to parse flag") util.HandleError(err, "Unable to parse flag")
} }
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs}) secretsPath, err := cmd.Flags().GetString("path")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, SecretsPath: secretsPath})
if err != nil { if err != nil {
util.HandleError(err, "Could not fetch secrets", "If you are using a service token to fetch secrets, please ensure it is valid") util.HandleError(err, "Could not fetch secrets", "If you are using a service token to fetch secrets, please ensure it is valid")
@@ -184,6 +189,7 @@ func init() {
runCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets, if any, with the same name over shared secrets") runCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets, if any, with the same name over shared secrets")
runCmd.Flags().StringP("command", "c", "", "chained commands to execute (e.g. \"npm install && npm run dev; echo ...\")") runCmd.Flags().StringP("command", "c", "", "chained commands to execute (e.g. \"npm install && npm run dev; echo ...\")")
runCmd.Flags().StringP("tags", "t", "", "filter secrets by tag slugs ") runCmd.Flags().StringP("tags", "t", "", "filter secrets by tag slugs ")
runCmd.Flags().String("path", "/", "get secrets within a folder path")
} }
// Will execute a single command and pass in the given secrets into the process // Will execute a single command and pass in the given secrets into the process

View File

@@ -44,6 +44,11 @@ var secretsCmd = &cobra.Command{
util.HandleError(err, "Unable to parse flag") util.HandleError(err, "Unable to parse flag")
} }
secretsPath, err := cmd.Flags().GetString("path")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
shouldExpandSecrets, err := cmd.Flags().GetBool("expand") shouldExpandSecrets, err := cmd.Flags().GetBool("expand")
if err != nil { if err != nil {
util.HandleError(err) util.HandleError(err)
@@ -54,7 +59,7 @@ var secretsCmd = &cobra.Command{
util.HandleError(err, "Unable to parse flag") util.HandleError(err, "Unable to parse flag")
} }
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs}) secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, SecretsPath: secretsPath})
if err != nil { if err != nil {
util.HandleError(err) util.HandleError(err)
} }
@@ -103,6 +108,11 @@ var secretsSetCmd = &cobra.Command{
} }
} }
secretsPath, err := cmd.Flags().GetString("path")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
workspaceFile, err := util.GetWorkSpaceFromFile() workspaceFile, err := util.GetWorkSpaceFromFile()
if err != nil { if err != nil {
util.HandleError(err, "Unable to get your local config details") util.HandleError(err, "Unable to get your local config details")
@@ -140,7 +150,7 @@ var secretsSetCmd = &cobra.Command{
plainTextEncryptionKey := crypto.DecryptAsymmetric(encryptedWorkspaceKey, encryptedWorkspaceKeyNonce, encryptedWorkspaceKeySenderPublicKey, currentUsersPrivateKey) plainTextEncryptionKey := crypto.DecryptAsymmetric(encryptedWorkspaceKey, encryptedWorkspaceKeyNonce, encryptedWorkspaceKeySenderPublicKey, currentUsersPrivateKey)
// pull current secrets // pull current secrets
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName}) secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, SecretsPath: secretsPath})
if err != nil { if err != nil {
util.HandleError(err, "unable to retrieve secrets") util.HandleError(err, "unable to retrieve secrets")
} }
@@ -191,6 +201,8 @@ var secretsSetCmd = &cobra.Command{
SecretValueIV: base64.StdEncoding.EncodeToString(encryptedValue.Nonce), SecretValueIV: base64.StdEncoding.EncodeToString(encryptedValue.Nonce),
SecretValueTag: base64.StdEncoding.EncodeToString(encryptedValue.AuthTag), SecretValueTag: base64.StdEncoding.EncodeToString(encryptedValue.AuthTag),
SecretValueHash: hashedValue, SecretValueHash: hashedValue,
PlainTextKey: key,
Type: existingSecret.Type,
} }
// Only add to modifications if the value is different // Only add to modifications if the value is different
@@ -222,6 +234,7 @@ var secretsSetCmd = &cobra.Command{
SecretValueTag: base64.StdEncoding.EncodeToString(encryptedValue.AuthTag), SecretValueTag: base64.StdEncoding.EncodeToString(encryptedValue.AuthTag),
SecretValueHash: hashedValue, SecretValueHash: hashedValue,
Type: util.SECRET_TYPE_SHARED, Type: util.SECRET_TYPE_SHARED,
PlainTextKey: key,
} }
secretsToCreate = append(secretsToCreate, encryptedSecretDetails) secretsToCreate = append(secretsToCreate, encryptedSecretDetails)
secretOperations = append(secretOperations, SecretSetOperation{ secretOperations = append(secretOperations, SecretSetOperation{
@@ -232,30 +245,43 @@ var secretsSetCmd = &cobra.Command{
} }
} }
if len(secretsToCreate) > 0 { for _, secret := range secretsToCreate {
batchCreateRequest := api.BatchCreateSecretsByWorkspaceAndEnvRequest{ createSecretRequest := api.CreateSecretV3Request{
WorkspaceId: workspaceFile.WorkspaceId, WorkspaceID: workspaceFile.WorkspaceId,
Environment: environmentName, Environment: environmentName,
Secrets: secretsToCreate, SecretName: secret.PlainTextKey,
SecretKeyCiphertext: secret.SecretKeyCiphertext,
SecretKeyIV: secret.SecretKeyIV,
SecretKeyTag: secret.SecretKeyTag,
SecretValueCiphertext: secret.SecretValueCiphertext,
SecretValueIV: secret.SecretValueIV,
SecretValueTag: secret.SecretValueTag,
Type: secret.Type,
SecretPath: secretsPath,
} }
err = api.CallBatchCreateSecretsByWorkspaceAndEnv(httpClient, batchCreateRequest) err = api.CallCreateSecretsV3(httpClient, createSecretRequest)
if err != nil { if err != nil {
util.HandleError(err, "Unable to process new secret creations") util.HandleError(err, "Unable to process new secret creations")
return return
} }
} }
if len(secretsToModify) > 0 { for _, secret := range secretsToModify {
batchModifyRequest := api.BatchModifySecretsByWorkspaceAndEnvRequest{ updateSecretRequest := api.UpdateSecretByNameV3Request{
WorkspaceId: workspaceFile.WorkspaceId, WorkspaceID: workspaceFile.WorkspaceId,
Environment: environmentName, Environment: environmentName,
Secrets: secretsToModify, SecretName: secret.PlainTextKey,
SecretValueCiphertext: secret.SecretValueCiphertext,
SecretValueIV: secret.SecretValueIV,
SecretValueTag: secret.SecretValueTag,
Type: secret.Type,
SecretPath: secretsPath,
} }
err = api.CallBatchModifySecretsByWorkspaceAndEnv(httpClient, batchModifyRequest) err = api.CallUpdateSecretsV3(httpClient, updateSecretRequest)
if err != nil { if err != nil {
util.HandleError(err, "Unable to process the modifications to your secrets") util.HandleError(err, "Unable to process secret update request")
return return
} }
} }
@@ -288,6 +314,16 @@ var secretsDeleteCmd = &cobra.Command{
} }
} }
secretsPath, err := cmd.Flags().GetString("path")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
secretType, err := cmd.Flags().GetString("type")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails() loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails()
if err != nil { if err != nil {
util.HandleError(err, "Unable to authenticate") util.HandleError(err, "Unable to authenticate")
@@ -298,46 +334,28 @@ var secretsDeleteCmd = &cobra.Command{
util.HandleError(err, "Unable to get local project details") util.HandleError(err, "Unable to get local project details")
} }
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName}) for _, secretName := range args {
if err != nil { request := api.DeleteSecretV3Request{
util.HandleError(err, "Unable to fetch secrets") WorkspaceId: workspaceFile.WorkspaceId,
} Environment: environmentName,
SecretName: secretName,
secretByKey := getSecretsByKeys(secrets) Type: secretType,
validSecretIdsToDelete := []string{} SecretPath: secretsPath,
invalidSecretNamesThatDoNotExist := []string{}
for _, secretKeyFromArg := range args {
if value, ok := secretByKey[strings.ToUpper(secretKeyFromArg)]; ok {
validSecretIdsToDelete = append(validSecretIdsToDelete, value.ID)
} else {
invalidSecretNamesThatDoNotExist = append(invalidSecretNamesThatDoNotExist, secretKeyFromArg)
} }
}
if len(invalidSecretNamesThatDoNotExist) != 0 { httpClient := resty.New().
message := fmt.Sprintf("secret name(s) [%v] does not exist in your project. To see which secrets exist run [infisical secrets]", strings.Join(invalidSecretNamesThatDoNotExist, ", ")) SetAuthToken(loggedInUserDetails.UserCredentials.JTWToken).
util.PrintErrorMessageAndExit(message) SetHeader("Accept", "application/json")
}
request := api.BatchDeleteSecretsBySecretIdsRequest{ err = api.CallDeleteSecretsV3(httpClient, request)
WorkspaceId: workspaceFile.WorkspaceId, if err != nil {
EnvironmentName: environmentName, util.HandleError(err, "Unable to complete your delete request")
SecretIds: validSecretIdsToDelete, }
}
httpClient := resty.New().
SetAuthToken(loggedInUserDetails.UserCredentials.JTWToken).
SetHeader("Accept", "application/json")
err = api.CallBatchDeleteSecretsByWorkspaceAndEnv(httpClient, request)
if err != nil {
util.HandleError(err, "Unable to complete your batch delete request")
} }
fmt.Printf("secret name(s) [%v] have been deleted from your project \n", strings.Join(args, ", ")) fmt.Printf("secret name(s) [%v] have been deleted from your project \n", strings.Join(args, ", "))
Telemetry.CaptureEvent("cli-command:secrets delete", posthog.NewProperties().Set("secretCount", len(secrets)).Set("version", util.CLI_VERSION)) Telemetry.CaptureEvent("cli-command:secrets delete", posthog.NewProperties().Set("secretCount", len(args)).Set("version", util.CLI_VERSION))
}, },
} }
@@ -611,11 +629,15 @@ func init() {
secretsCmd.AddCommand(secretsGetCmd) secretsCmd.AddCommand(secretsGetCmd)
secretsCmd.AddCommand(secretsSetCmd) secretsCmd.AddCommand(secretsSetCmd)
secretsSetCmd.Flags().String("path", "/", "get secrets within a folder path")
secretsSetCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) { secretsSetCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
util.RequireLogin() util.RequireLogin()
util.RequireLocalWorkspaceFile() util.RequireLocalWorkspaceFile()
} }
secretsDeleteCmd.Flags().String("type", "personal", "the type of secret to delete: personal or shared (default: personal)")
secretsDeleteCmd.Flags().String("path", "/", "get secrets within a folder path")
secretsCmd.AddCommand(secretsDeleteCmd) secretsCmd.AddCommand(secretsDeleteCmd)
secretsDeleteCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) { secretsDeleteCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
util.RequireLogin() util.RequireLogin()
@@ -626,5 +648,6 @@ func init() {
secretsCmd.PersistentFlags().String("env", "dev", "Used to select the environment name on which actions should be taken on") secretsCmd.PersistentFlags().String("env", "dev", "Used to select the environment name on which actions should be taken on")
secretsCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets") secretsCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets")
secretsCmd.PersistentFlags().StringP("tags", "t", "", "filter secrets by tag slugs") secretsCmd.PersistentFlags().StringP("tags", "t", "", "filter secrets by tag slugs")
secretsCmd.Flags().String("path", "/", "get secrets within a folder path")
rootCmd.AddCommand(secretsCmd) rootCmd.AddCommand(secretsCmd)
} }

View File

@@ -5,9 +5,10 @@ import (
) )
type UserCredentials struct { type UserCredentials struct {
Email string `json:"email"` Email string `json:"email"`
PrivateKey string `json:"privateKey"` PrivateKey string `json:"privateKey"`
JTWToken string `json:"JTWToken"` JTWToken string `json:"JTWToken"`
RefreshToken string `json:"RefreshToken"`
} }
// The file struct for Infisical config file // The file struct for Infisical config file
@@ -63,4 +64,5 @@ type GetAllSecretsParameters struct {
InfisicalToken string InfisicalToken string
TagSlugs string TagSlugs string
WorkspaceId string WorkspaceId string
SecretsPath string
} }

View File

@@ -9,6 +9,7 @@ import (
"github.com/Infisical/infisical-merge/packages/config" "github.com/Infisical/infisical-merge/packages/config"
"github.com/Infisical/infisical-merge/packages/models" "github.com/Infisical/infisical-merge/packages/models"
"github.com/go-resty/resty/v2" "github.com/go-resty/resty/v2"
"github.com/rs/zerolog/log"
) )
type LoggedInUserDetails struct { type LoggedInUserDetails struct {
@@ -96,6 +97,20 @@ func GetCurrentLoggedInUserDetails() (LoggedInUserDetails, error) {
} }
isAuthenticated := api.CallIsAuthenticated(httpClient) isAuthenticated := api.CallIsAuthenticated(httpClient)
if !isAuthenticated {
accessTokenResponse, _ := api.CallGetNewAccessTokenWithRefreshToken(httpClient, userCreds.RefreshToken)
if accessTokenResponse.Token != "" {
isAuthenticated = true
userCreds.JTWToken = accessTokenResponse.Token
}
}
err = StoreUserCredsInKeyRing(&userCreds)
if err != nil {
log.Debug().Msg("unable to store your user credentials with new access token")
}
if !isAuthenticated { if !isAuthenticated {
return LoggedInUserDetails{ return LoggedInUserDetails{
IsUserLoggedIn: true, // was logged in IsUserLoggedIn: true, // was logged in

View File

@@ -74,23 +74,12 @@ func ConfigContainsEmail(users []models.LoggedInUser, email string) bool {
} }
func RequireLogin() { func RequireLogin() {
currentUserDetails, err := GetCurrentLoggedInUserDetails() // get the config file that stores the current logged in user email
configFile, _ := GetConfigFile()
if err != nil { if configFile.LoggedInUserEmail == "" {
HandleError(err, "unable to retrieve your login details")
}
if !currentUserDetails.IsUserLoggedIn {
PrintErrorMessageAndExit("You must be logged in to run this command. To login, run [infisical login]") PrintErrorMessageAndExit("You must be logged in to run this command. To login, run [infisical login]")
} }
if currentUserDetails.LoginExpired {
PrintErrorMessageAndExit("Your login expired, please login in again. To login, run [infisical login]")
}
if currentUserDetails.UserCredentials.Email == "" && currentUserDetails.UserCredentials.JTWToken == "" && currentUserDetails.UserCredentials.PrivateKey == "" {
PrintErrorMessageAndExit("One or more of your login details is empty. Please try logging in again via by running [infisical login]")
}
} }
func RequireServiceToken() { func RequireServiceToken() {

View File

@@ -34,7 +34,7 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string) ([]models.Singl
return nil, api.GetServiceTokenDetailsResponse{}, fmt.Errorf("unable to get service token details. [err=%v]", err) return nil, api.GetServiceTokenDetailsResponse{}, fmt.Errorf("unable to get service token details. [err=%v]", err)
} }
encryptedSecrets, err := api.CallGetSecretsV2(httpClient, api.GetEncryptedSecretsV2Request{ encryptedSecrets, err := api.CallGetSecretsV3(httpClient, api.GetEncryptedSecretsV3Request{
WorkspaceId: serviceTokenDetails.Workspace, WorkspaceId: serviceTokenDetails.Workspace,
Environment: serviceTokenDetails.Environment, Environment: serviceTokenDetails.Environment,
}) })
@@ -61,7 +61,7 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string) ([]models.Singl
return plainTextSecrets, serviceTokenDetails, nil return plainTextSecrets, serviceTokenDetails, nil
} }
func GetPlainTextSecretsViaJTW(JTWToken string, receiversPrivateKey string, workspaceId string, environmentName string, tagSlugs string) ([]models.SingleEnvironmentVariable, error) { func GetPlainTextSecretsViaJTW(JTWToken string, receiversPrivateKey string, workspaceId string, environmentName string, tagSlugs string, secretsPath string) ([]models.SingleEnvironmentVariable, error) {
httpClient := resty.New() httpClient := resty.New()
httpClient.SetAuthToken(JTWToken). httpClient.SetAuthToken(JTWToken).
SetHeader("Accept", "application/json") SetHeader("Accept", "application/json")
@@ -102,11 +102,17 @@ func GetPlainTextSecretsViaJTW(JTWToken string, receiversPrivateKey string, work
plainTextWorkspaceKey := crypto.DecryptAsymmetric(encryptedWorkspaceKey, encryptedWorkspaceKeyNonce, encryptedWorkspaceKeySenderPublicKey, currentUsersPrivateKey) plainTextWorkspaceKey := crypto.DecryptAsymmetric(encryptedWorkspaceKey, encryptedWorkspaceKeyNonce, encryptedWorkspaceKeySenderPublicKey, currentUsersPrivateKey)
encryptedSecrets, err := api.CallGetSecretsV2(httpClient, api.GetEncryptedSecretsV2Request{ getSecretsRequest := api.GetEncryptedSecretsV3Request{
WorkspaceId: workspaceId, WorkspaceId: workspaceId,
Environment: environmentName, Environment: environmentName,
TagSlugs: tagSlugs, // TagSlugs: tagSlugs,
}) }
if secretsPath != "" {
getSecretsRequest.SecretPath = secretsPath
}
encryptedSecrets, err := api.CallGetSecretsV3(httpClient, getSecretsRequest)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -162,7 +168,7 @@ func GetAllEnvironmentVariables(params models.GetAllSecretsParameters) ([]models
return nil, fmt.Errorf("unable to validate environment name because [err=%s]", err) return nil, fmt.Errorf("unable to validate environment name because [err=%s]", err)
} }
secretsToReturn, errorToReturn = GetPlainTextSecretsViaJTW(loggedInUserDetails.UserCredentials.JTWToken, loggedInUserDetails.UserCredentials.PrivateKey, workspaceFile.WorkspaceId, params.Environment, params.TagSlugs) secretsToReturn, errorToReturn = GetPlainTextSecretsViaJTW(loggedInUserDetails.UserCredentials.JTWToken, loggedInUserDetails.UserCredentials.PrivateKey, workspaceFile.WorkspaceId, params.Environment, params.TagSlugs, params.SecretsPath)
log.Debug().Msgf("GetAllEnvironmentVariables: Trying to fetch secrets JTW token [err=%s]", errorToReturn) log.Debug().Msgf("GetAllEnvironmentVariables: Trying to fetch secrets JTW token [err=%s]", errorToReturn)
backupSecretsEncryptionKey := []byte(loggedInUserDetails.UserCredentials.PrivateKey)[0:32] backupSecretsEncryptionKey := []byte(loggedInUserDetails.UserCredentials.PrivateKey)[0:32]
@@ -333,7 +339,7 @@ func OverrideSecrets(secrets []models.SingleEnvironmentVariable, secretType stri
return secretsToReturn return secretsToReturn
} }
func GetPlainTextSecrets(key []byte, encryptedSecrets api.GetEncryptedSecretsV2Response) ([]models.SingleEnvironmentVariable, error) { func GetPlainTextSecrets(key []byte, encryptedSecrets api.GetEncryptedSecretsV3Response) ([]models.SingleEnvironmentVariable, error) {
plainTextSecrets := []models.SingleEnvironmentVariable{} plainTextSecrets := []models.SingleEnvironmentVariable{}
for _, secret := range encryptedSecrets.Secrets { for _, secret := range encryptedSecrets.Secrets {
// Decrypt key // Decrypt key

View File

@@ -3,32 +3,29 @@ title: "Authentication"
description: "How to authenticate with the Infisical Public API" description: "How to authenticate with the Infisical Public API"
--- ---
## Essentials The Public API accepts multiple modes of authentication being via [Infisical Token](/documentation/platform/token) or API Key.
The Public API accepts multiple modes of authentication being via API Key or [Infisical Token](/documentation/platform/token).
- API Key: Provides full access to all endpoints representing the user without ability to encrypt/decrypt secrets in **E2EE** mode.
- [Infisical Token](/documentation/platform/token): Provides short-lived, scoped CRUD access to the secrets of a specific project and environment. - [Infisical Token](/documentation/platform/token): Provides short-lived, scoped CRUD access to the secrets of a specific project and environment.
- API Key: Provides full access to all endpoints representing the user without ability to encrypt/decrypt secrets for **E2EE** endpoints.
<AccordionGroup> <Tabs>
<Accordion title="API Key"> <Tab title="Infisical Token">
The API key mode uses an API key to authenticate with the API. The Infisical Token mode uses an Infisical Token to authenticate with the API.
To authenticate requests with Infisical using the API Key, you must include an API key in the `X-API-KEY` header of HTTP requests made to the platform. To authenticate requests with Infisical using the Infisical Token, you must include your Infisical Token in the `Authorization` header of HTTP requests made to the platform with the value `Bearer <infisical_token>`.
You can obtain an API key in User Settings > API Keys You can obtain an Infisical Token in Project Settings > Service Tokens.
![API key dashboard](../../images/api-key-dashboard.png) ![token add](../../images/project-token-add.png)
![API key in personal settings](../../images/api-key-settings.png) </Tab>
</Accordion> <Tab title="API Key">
<Accordion title="Infisical Token"> The API key mode uses an API key to authenticate with the API.
The Infisical Token mode uses an Infisical Token to authenticate with the API. To authenticate requests with Infisical using the API Key, you must include an API key in the `X-API-KEY` header of HTTP requests made to the platform.
To authenticate requests with Infisical using the Infisical Token, you must include your Infisical Token in the `Authorization` header of HTTP requests made to the platform with the value `Bearer <infisical_token>`. You can obtain an API key in User Settings > API Keys
You can obtain an Infisical Token in Project Settings > Service Tokens. ![API key dashboard](../../images/api-key-dashboard.png)
![API key in personal settings](../../images/api-key-settings.png)
![token add](../../images/project-token-add.png) </Tab>
</Accordion> </Tabs>
</AccordionGroup>

View File

@@ -1,92 +0,0 @@
---
title: "ES Mode"
---
Encrypted Standard (ES) mode is the easiest way to use Infisical's API. With it, you can make HTTP calls to Infisical
to read/write secrets in plaintext.
Prerequisites:
- Set up and add envars to [Infisical Cloud](https://app.infisical.com).
- Create an [Infisical Token](/documentation/platform/token) for your project and environment with write access enabled.
- [Ensure that your project is blind-indexed](../blind-indices).
Below, we showcase how to execute common CRUD operations to manage secrets in **ES** mode:
<AccordionGroup>
<Accordion title="Retrieve secrets">
<Tabs>
<Tab title="cURL">
```bash
curl --location --request GET 'http://localhost:8080/api/v3/secrets/raw?environment=dev&workspaceId=xxx' \
--header 'Authorization: Bearer st.xxx'
```
</Tab>
</Tabs>
</Accordion>
<Accordion title="Create secret">
<Tabs>
<Tab title="cURL">
```bash
curl --location --request POST 'http://localhost:8080/api/v3/secrets/raw/SECRET_NAME' \
--header 'Authorization: Bearer st.xxx' \
--header 'Content-Type: application/json' \
--data-raw '{
"workspaceId": "xxx",
"environment": "dev",
"type": "shared",
"secretValue": "SECRET_VALUE",
"secretPath": "/"
}'
```
</Tab>
</Tabs>
</Accordion>
<Accordion title="Retrieve secret">
<Tabs>
<Tab title="cURL">
```bash
curl --location --request GET 'http://localhost:8080/api/v3/secrets/raw/SECRET_NAME?workspaceId=xxx&environment=dev&secretPath=/' \
--header 'Authorization: Bearer st.xxx'
```
</Tab>
</Tabs>
</Accordion>
<Accordion title="Update secret">
<Tabs>
<Tab title="cURL">
```bash
curl --location --request PATCH 'http://localhost:8080/api/v3/secrets/raw/SECRET_NAME' \
--header 'Authorization: Bearer st.xxx' \
--header 'Content-Type: application/json' \
--data-raw '{
"workspaceId": "xxx",
"environment": "dev",
"type": "shared",
"secretValue": "SECRET_VALUE",
"secretPath": "/"
}'
```
</Tab>
</Tabs>
</Accordion>
<Accordion title="Delete secret">
<Tabs>
<Tab title="cURL">
```bash
curl --location --request DELETE 'http://localhost:8080/api/v3/secrets/raw/SECRET_NAME' \
--header 'Authorization: Bearer st.xxx' \
--header 'Content-Type: application/json' \
--data-raw '{
"workspaceId": "xxx",
"environment": "dev",
"type": "shared",
"secretValue": "SECRET_VALUE",
"secretPath": "/"
}'
```
</Tab>
</Tabs>
</Accordion>
</AccordionGroup>

View File

@@ -1,57 +0,0 @@
---
title: "Preface"
---
Each project in Infisical can be used either in **End-to-End Encrypted (E2EE)** mode or **Encrypted Standard (ES)** mode which dictates how it can be interacted with via the Infisical API.
<CardGroup cols={2}>
<Card
title="Encrypted Standard (ES)"
href="/api-reference/overview/encryption-modes/es-mode"
icon="shield-halved"
color="#3c8639"
>
Secret operations without client-side encryption/decryption
</Card>
<Card href="/api-reference/overview/encryption-modes/e2ee-mode" title="End-to-End Encrypted (E2EE)" icon="shield" color="#3775a9">
Secret operations with client-side encryption/decryption
</Card>
</CardGroup>
By default, all projects are initialized in **E2EE** mode which means the server is not able to decrypt any values because all secret encryption/decryption operations occur on the client-side. However, this has limitations around functionality and ease-of-use:
- You cannot make HTTP calls to Infisical to read/write secrets in plaintext.
- You cannot leverage non-E2EE features like native integrations and in-platform automations like dynamic secrets and secret rotation.
For this reason, Infisical also provides the **ES** mode of operation to unlock the above limitations by enabling the server to decrypt your values. You can optionally switch a project to using **ES** mode
in your Project Settings.
<Note>
Make no mistake, the limitations of **E2EE** mode do not prevent you from syncing secrets from Infisical to platforms like GitLab. They just imply
that you have to do things the "E2EE-way" such as by embedding the Infisical CLI into your GitLab CI/CD pipelines to fetch and decrypt
secrets on the client-side.
</Note>
## FAQ
<AccordionGroup>
<Accordion title="Is E2EE mode or ES mode right for me?">
We recommend starting with **E2EE** mode and switching to **ES** mode when:
- Your team needs more power out of non-E2EE features available in **ES** mode such as secret rotation, dynamic secrets, etc.
- Your team wants an easier way to read/write secrets with Infisical.
</Accordion>
<Accordion title="How can I switch from E2EE mode to ES mode?">
By default, all projects in Infisical are initialized to **E2EE** mode and can be switched to **ES** mode in the Project Settings by disabling end-to-end encryption.
</Accordion>
<Accordion title="Is ES mode secure if it's not E2EE?">
**ES** mode is secure and in fact what most vendors in the secret management industry are doing at the moment. In this mode, secrets are encrypted at rest by
a series of keys, secured ultimately by a top-level `ROOT_ENCRYPTION_KEY` located on the server.
If you're concerned about Infisical Cloud's ability to read your secrets if using **ES** mode in Infisical Cloud, then you may wish to
use Infisical Cloud in **E2EE** mode or self-host Infisical on your own infrastructure and then use **ES** mode; this of course which means setting up firewalls and securing the instance yourself.
As an organization, we prohibit reading any customer secrets without explicit permission; access to the `ROOT_ENCRYPTION_KEY` is restricted to one individual in the organization.
</Accordion>
</AccordionGroup>

View File

@@ -1,233 +0,0 @@
---
title: "Create secret"
description: "How to add a secret using an Infisical Token scoped to a project and environment"
---
Prerequisites:
- Set up and add envars to [Infisical Cloud](https://app.infisical.com).
- Create an [Infisical Token](/documentation/platform/token) for your project and environment with write access enabled.
- Grasp a basic understanding of the system and its underlying cryptography [here](/api-reference/overview/introduction).
- [Ensure that your project is blind-indexed](../blind-indices).
## Flow
1. [Get your Infisical Token data](/api-reference/endpoints/service-tokens/get) including a (encrypted) project key.
2. Decrypt the (encrypted) project key with the key from your Infisical Token.
3. Encrypt your secret with the project key
4. [Send (encrypted) secret to Infisical](/api-reference/endpoints/secrets/create)
## Example
<Tabs>
<Tab title="Javascript">
```js
const crypto = require('crypto');
const axios = require('axios');
const nacl = require('tweetnacl');
const BASE_URL = 'https://app.infisical.com';
const ALGORITHM = 'aes-256-gcm';
const BLOCK_SIZE_BYTES = 16;
const encrypt = ({ text, secret }) => {
const iv = crypto.randomBytes(BLOCK_SIZE_BYTES);
const cipher = crypto.createCipheriv(ALGORITHM, secret, iv);
let ciphertext = cipher.update(text, 'utf8', 'base64');
ciphertext += cipher.final('base64');
return {
ciphertext,
iv: iv.toString('base64'),
tag: cipher.getAuthTag().toString('base64')
};
}
const decrypt = ({ ciphertext, iv, tag, secret}) => {
const decipher = crypto.createDecipheriv(
ALGORITHM,
secret,
Buffer.from(iv, 'base64')
);
decipher.setAuthTag(Buffer.from(tag, 'base64'));
let cleartext = decipher.update(ciphertext, 'base64', 'utf8');
cleartext += decipher.final('utf8');
return cleartext;
}
const createSecrets = async () => {
const serviceToken = '';
const serviceTokenSecret = serviceToken.substring(serviceToken.lastIndexOf('.') + 1);
const secretType = 'shared'; // 'shared' or 'personal'
const secretKey = 'some_key';
const secretValue = 'some_value';
const secretComment = 'some_comment';
// 1. Get your Infisical Token data
const { data: serviceTokenData } = await axios.get(
`${BASE_URL}/api/v2/service-token`,
{
headers: {
Authorization: `Bearer ${serviceToken}`
}
}
);
// 2. Decrypt the (encrypted) project key with the key from your Infisical Token
const projectKey = decrypt({
ciphertext: serviceTokenData.encryptedKey,
iv: serviceTokenData.iv,
tag: serviceTokenData.tag,
secret: serviceTokenSecret
});
// 3. Encrypt your secret with the project key
const {
ciphertext: secretKeyCiphertext,
iv: secretKeyIV,
tag: secretKeyTag
} = encrypt({
text: secretKey,
secret: projectKey
});
const {
ciphertext: secretValueCiphertext,
iv: secretValueIV,
tag: secretValueTag
} = encrypt({
text: secretValue,
secret: projectKey
});
const {
ciphertext: secretCommentCiphertext,
iv: secretCommentIV,
tag: secretCommentTag
} = encrypt({
text: secretComment,
secret: projectKey
});
// 4. Send (encrypted) secret to Infisical
await axios.post(
`${BASE_URL}/api/v3/secrets/${secretKey}`,
{
workspaceId: serviceTokenData.workspace,
environment: serviceTokenData.environment,
type: secretType,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag
},
{
headers: {
Authorization: `Bearer ${serviceToken}`
}
}
);
}
createSecrets();
```
</Tab>
<Tab title="Python">
```Python
import base64
import requests
from Cryptodome.Cipher import AES
from Cryptodome.Random import get_random_bytes
BASE_URL = "https://app.infisical.com"
BLOCK_SIZE_BYTES = 16
def encrypt(text, secret):
iv = get_random_bytes(BLOCK_SIZE_BYTES)
secret = bytes(secret, "utf-8")
cipher = AES.new(secret, AES.MODE_GCM, iv)
ciphertext, tag = cipher.encrypt_and_digest(text.encode("utf-8"))
return {
"ciphertext": base64.standard_b64encode(ciphertext).decode("utf-8"),
"tag": base64.standard_b64encode(tag).decode("utf-8"),
"iv": base64.standard_b64encode(iv).decode("utf-8"),
}
def decrypt(ciphertext, iv, tag, secret):
secret = bytes(secret, "utf-8")
iv = base64.standard_b64decode(iv)
tag = base64.standard_b64decode(tag)
ciphertext = base64.standard_b64decode(ciphertext)
cipher = AES.new(secret, AES.MODE_GCM, iv)
cipher.update(tag)
cleartext = cipher.decrypt(ciphertext).decode("utf-8")
return cleartext
def create_secrets():
service_token = "your_service_token"
service_token_secret = service_token[service_token.rindex(".") + 1 :]
secret_type = "shared" # "shared or "personal"
secret_key = "some_key"
secret_value = "some_value"
secret_comment = "some_comment"
# 1. Get your Infisical Token data
service_token_data = requests.get(
f"{BASE_URL}/api/v2/service-token",
headers={"Authorization": f"Bearer {service_token}"},
).json()
# 2. Decrypt the (encrypted) project key with the key from your Infisical Token
project_key = decrypt(
ciphertext=service_token_data["encryptedKey"],
iv=service_token_data["iv"],
tag=service_token_data["tag"],
secret=service_token_secret,
)
# 3. Encrypt your secret with the project key
encrypted_key_data = encrypt(text=secret_key, secret=project_key)
encrypted_value_data = encrypt(text=secret_value, secret=project_key)
encrypted_comment_data = encrypt(text=secret_comment, secret=project_key)
# 4. Send (encrypted) secret to Infisical
requests.post(
f"{BASE_URL}/api/v3/secrets/{secret_key}",
json={
"workspaceId": service_token_data["workspace"],
"environment": service_token_data["environment"],
"type": secret_type,
"secretKeyCiphertext": encrypted_key_data["ciphertext"],
"secretKeyIV": encrypted_key_data["iv"],
"secretKeyTag": encrypted_key_data["tag"],
"secretValueCiphertext": encrypted_value_data["ciphertext"],
"secretValueIV": encrypted_value_data["iv"],
"secretValueTag": encrypted_value_data["tag"],
"secretCommentCiphertext": encrypted_comment_data["ciphertext"],
"secretCommentIV": encrypted_comment_data["iv"],
"secretCommentTag": encrypted_comment_data["tag"]
},
headers={"Authorization": f"Bearer {service_token}"},
)
create_secrets()
```
</Tab>
</Tabs>

View File

@@ -1,94 +0,0 @@
---
title: "Delete secret"
description: "How to delete a secret using an Infisical Token scoped to a project and environment"
---
Prerequisites:
- Set up and add envars to [Infisical Cloud](https://app.infisical.com).
- Create either an [API Key](/api-reference/overview/authentication) or [Infisical Token](/documentation/platform/token) for your project and environment with write access enabled.
- Grasp a basic understanding of the system and its underlying cryptography [here](/api-reference/overview/introduction).
- [Ensure that your project is blind-indexed](../blind-indices).
## Example
<Tabs>
<Tab title="Javascript">
```js
const axios = require('axios');
const BASE_URL = 'https://app.infisical.com';
const deleteSecrets = async () => {
const serviceToken = 'your_service_token';
const secretType = 'shared' // 'shared' or 'personal'
const secretKey = 'some_key'
// 1. Get your Infisical Token data
const { data: serviceTokenData } = await axios.get(
`${BASE_URL}/api/v2/service-token`,
{
headers: {
Authorization: `Bearer ${serviceToken}`
}
}
);
// 2. Delete secret from Infisical
await axios.delete(
`${BASE_URL}/api/v3/secrets/${secretKey}`,
{
workspaceId: serviceTokenData.workspace,
environment: serviceTokenData.environment,
type: secretType
},
{
headers: {
Authorization: `Bearer ${serviceToken}`
},
}
);
};
deleteSecrets();
```
</Tab>
<Tab title="Python">
```Python
import requests
BASE_URL = "https://app.infisical.com"
def delete_secrets():
service_token = "<your_service_token>"
secret_type = "shared" # "shared" or "personal"
secret_key = "some_key"
# 1. Get your Infisical Token data
service_token_data = requests.get(
f"{BASE_URL}/api/v2/service-token",
headers={"Authorization": f"Bearer {service_token}"},
).json()
# 2. Delete secret from Infisical
requests.delete(
f"{BASE_URL}/api/v2/secrets/{secret_key}",
json={
"workspaceId": service_token_data["workspace"],
"environment": service_token_data["environment"],
"type": secret_type
},
headers={"Authorization": f"Bearer {service_token}"},
)
delete_secrets()
```
</Tab>
</Tabs>
<Info>
If using an `API_KEY` to authenticate with the Infisical API, then you should include it in the `X_API_KEY` header.
</Info>

View File

@@ -0,0 +1,176 @@
---
title: "E2EE Disabled"
---
Using Infisical's API to read/write secrets with E2EE disabled allows you to create, update, and retrieve secrets
in plaintext. Effectively, this means each such secret operation only requires 1 HTTP call.
<AccordionGroup>
<Accordion title="Retrieve secrets">
Retrieve all secrets for an Infisical project and environment.
<Tabs>
<Tab title="cURL">
```bash
curl --location --request GET 'https://app.infisical.com/api/v3/secrets/raw?environment=environment&workspaceId=workspaceId' \
--header 'Authorization: Bearer serviceToken'
```
</Tab>
</Tabs>
<ParamField query="workspaceId" type="string" required>
The ID of the workspace
</ParamField>
<ParamField query="environment" type="string" required>
The environment slug
</ParamField>
<ParamField query="secretPath" type="string" default="/" optional>
Path to secrets in workspace
</ParamField>
</Accordion>
<Accordion title="Create secret">
Create a secret in Infisical.
<Tabs>
<Tab title="cURL">
```bash
curl --location --request POST 'https://app.infisical.com/api/v3/secrets/raw/secretName' \
--header 'Authorization: Bearer serviceToken' \
--header 'Content-Type: application/json' \
--data-raw '{
"workspaceId": "workspaceId",
"environment": "environment",
"type": "shared",
"secretValue": "secretValue",
"secretPath": "/"
}'
```
</Tab>
</Tabs>
<ParamField path="secretName" type="string" required>
Name of secret to create
</ParamField>
<ParamField body="workspaceId" type="string" required>
The ID of the workspace
</ParamField>
<ParamField body="environment" type="string" required>
The environment slug
</ParamField>
<ParamField body="secretValue" type="string" required>
Value of secret
</ParamField>
<ParamField body="secretComment" type="string" optional>
Comment of secret
</ParamField>
<ParamField body="secretPath" type="string" default="/" optional>
Path to secret in workspace
</ParamField>
<ParamField query="type" type="string" optional default="shared">
The type of the secret. Valid options are “shared” or “personal”
</ParamField>
</Accordion>
<Accordion title="Retrieve secret">
Retrieve a secret from Infisical.
<Tabs>
<Tab title="cURL">
```bash
curl --location --request GET 'https://app.infisical.com/api/v3/secrets/raw/secretName?workspaceId=workspaceId&environment=environment' \
--header 'Authorization: Bearer serviceToken'
```
</Tab>
</Tabs>
<ParamField path="secretName" type="string" required>
Name of secret to retrieve
</ParamField>
<ParamField query="workspaceId" type="string" required>
The ID of the workspace
</ParamField>
<ParamField query="environment" type="string" required>
The environment slug
</ParamField>
<ParamField query="secretPath" type="string" default="/" optional>
Path to secrets in workspace
</ParamField>
<ParamField query="type" type="string" optional default="personal">
The type of the secret. Valid options are “shared” or “personal”
</ParamField>
</Accordion>
<Accordion title="Update secret">
Update an existing secret in Infisical.
<Tabs>
<Tab title="cURL">
```bash
curl --location --request PATCH 'https://app.infisical.com/api/v3/secrets/raw/secretName' \
--header 'Authorization: Bearer serviceToken' \
--header 'Content-Type: application/json' \
--data-raw '{
"workspaceId": "workspaceId",
"environment": "environment",
"type": "shared",
"secretValue": "secretValue",
"secretPath": "/"
}'
```
</Tab>
</Tabs>
<ParamField path="secretName" type="string" required>
Name of secret to update
</ParamField>
<ParamField body="workspaceId" type="string" required>
The ID of the workspace
</ParamField>
<ParamField body="environment" type="string" required>
The environment slug
</ParamField>
<ParamField body="secretValue" type="string" required>
Value of secret
</ParamField>
<ParamField body="secretPath" type="string" default="/" optional>
Path to secret in workspace.
</ParamField>
<ParamField query="type" type="string" optional default="shared">
The type of the secret. Valid options are “shared” or “personal”
</ParamField>
</Accordion>
<Accordion title="Delete secret">
Delete a secret in Infisical.
<Tabs>
<Tab title="cURL">
```bash
curl --location --request DELETE 'https://app.infisical.com/api/v3/secrets/raw/secretName' \
--header 'Authorization: Bearer serviceToken' \
--header 'Content-Type: application/json' \
--data-raw '{
"workspaceId": "workspaceId",
"environment": "environment",
"type": "shared",
"secretPath": "/"
}'
```
</Tab>
</Tabs>
<ParamField path="secretName" type="string" required>
Name of secret to update
</ParamField>
<ParamField body="workspaceId" type="string" required>
The ID of the workspace
</ParamField>
<ParamField body="environment" type="string" required>
The environment slug
</ParamField>
<ParamField body="secretPath" type="string" default="/" optional>
Path to secret in workspace.
</ParamField>
<ParamField query="type" type="string" optional default="personal">
The type of the secret. Valid options are “shared” or “personal”
</ParamField>
</Accordion>
</AccordionGroup>

View File

@@ -1,23 +1,16 @@
--- ---
title: "E2EE Mode" title: "E2EE Enabled"
--- ---
End-to-End Encrypted (E2EE) mode is the default way to use Infisical's API. With it, you must perform client-side encryption/decryption Using Infisical's API to read/write secrets with E2EE enabled allows you to create, update, and retrieve secrets
when reading/writing secrets via HTTP call to Infisical. but requires you to perform client-side encryption/decryption operations. For this reason, we recommend using one of the available
SDKs instead.
Prerequisites:
- Set up and add envars to [Infisical Cloud](https://app.infisical.com).
- Create an [Infisical Token](/documentation/platform/token) for your project and environment with write access enabled.
- Grasp a basic understanding of the system and its underlying cryptography [here](/api-reference/overview/introduction).
- [Ensure that your project is blind-indexed](../blind-indices).
Below, we showcase how to execute common CRUD operations to manage secrets in **E2EE** mode:
<AccordionGroup> <AccordionGroup>
<Accordion title="Retrieve secrets"> <Accordion title="Retrieve secrets">
<Tabs> <Tabs>
<Tab title="Javascript"> <Tab title="Javascript">
Retrieve all secrets for an Infisical project and environment.
```js ```js
const crypto = require('crypto'); const crypto = require('crypto');
const axios = require('axios'); const axios = require('axios');
@@ -194,6 +187,7 @@ get_secrets()
<Accordion title="Create secret"> <Accordion title="Create secret">
<Tabs> <Tabs>
<Tab title="Javascript"> <Tab title="Javascript">
Create a secret in Infisical.
```js ```js
const crypto = require('crypto'); const crypto = require('crypto');
const axios = require('axios'); const axios = require('axios');
@@ -408,6 +402,7 @@ create_secrets()
<Accordion title="Retrieve secret"> <Accordion title="Retrieve secret">
<Tabs> <Tabs>
<Tab title="Javascript"> <Tab title="Javascript">
Retrieve a secret from Infisical.
```js ```js
const crypto = require('crypto'); const crypto = require('crypto');
const axios = require('axios'); const axios = require('axios');
@@ -569,6 +564,7 @@ get_secret()
<Accordion title="Update secret"> <Accordion title="Update secret">
<Tabs> <Tabs>
<Tab title="Javascript"> <Tab title="Javascript">
Update an existing secret in Infisical.
```js ```js
const crypto = require('crypto'); const crypto = require('crypto');
const axios = require('axios'); const axios = require('axios');
@@ -779,6 +775,7 @@ update_secret()
<Accordion title="Delete secret"> <Accordion title="Delete secret">
<Tabs> <Tabs>
<Tab title="Javascript"> <Tab title="Javascript">
Delete a secret in Infisical.
```js ```js
const axios = require('axios'); const axios = require('axios');
const BASE_URL = 'https://app.infisical.com'; const BASE_URL = 'https://app.infisical.com';

View File

@@ -0,0 +1,54 @@
---
title: "Note on E2EE"
---
Each project in Infisical can have **End-to-End Encryption (E2EE)** enabled or disabled.
By default, all projects have **E2EE** enabled which means the server is not able to decrypt any values because all secret encryption/decryption operations occur on the client-side; this can be (optionally) disabled. However, this has limitations around functionality and ease-of-use:
- You cannot make HTTP calls to Infisical to read/write secrets in plaintext.
- You cannot leverage non-E2EE features like native integrations and in-platform automations like dynamic secrets and secret rotation.
<CardGroup cols={2}>
<Card
title="E2EE Disabled"
href="/api-reference/overview/examples/e2ee-disabled"
icon="shield-halved"
color="#3c8639"
>
Example read/write secrets without client-side encryption/decryption
</Card>
<Card
href="/api-reference/overview/examples/e2ee-enabled"
title="E2EE Enabled"
icon="shield"
color="#3775a9"
>
Example read/write secrets with client-side encryption/decryption
</Card>
</CardGroup>
## FAQ
<AccordionGroup>
<Accordion title="Should I have E2EE enabled or disabled?">
We recommend starting with having **E2EE** enabled and disabling it if:
- You're self-hosting Infisical, so having your instance of Infisical be able to read your secrets isn't an issue.
- You want an easier way to read/write secrets with Infisical.
- You need more power out of non-E2EE features such as secret rotation, dynamic secrets, etc.
</Accordion>
<Accordion title="How can I enable/disable E2EE?">
You can enable/disable E2EE for your project in Infisical in the Project Settings.
</Accordion>
<Accordion title="Is disabling E2EE secure?">
It is secure and in fact how most vendors in our industry are able to offer features like secret rotation. In this mode, secrets are encrypted at rest by
a series of keys, secured ultimately by a top-level `ROOT_ENCRYPTION_KEY` located on the server.
If you're concerned about Infisical Cloud's ability to read your secrets, then you may wish to
use it with **E2EE** enabled or self-host Infisical on your own infrastructure and disable E2EE there.
As an organization, we do not read any customer secrets without explicit permission; access to the `ROOT_ENCRYPTION_KEY` is restricted to one individual in the organization.
</Accordion>
</AccordionGroup>

View File

@@ -1,180 +0,0 @@
---
title: "Retrieve secret"
description: "How to get a secret using an Infisical Token scoped to a project and environment"
---
Prerequisites:
- Set up and add envars to [Infisical Cloud](https://app.infisical.com).
- Create an [Infisical Token](/documentation/platform/token) for your project and environment.
- Grasp a basic understanding of the system and its underlying cryptography [here](/api-reference/overview/introduction).
- [Ensure that your project is blind-indexed](../blind-indices).
## Flow
1. [Get your Infisical Token data](/api-reference/endpoints/service-tokens/get) including a (encrypted) project key.
2. [Get the secret from your project and environment](/api-reference/endpoints/secrets/read-one).
3. Decrypt the (encrypted) project key with the key from your Infisical Token.
4. Decrypt the (encrypted) secret
## Example
<Tabs>
<Tab title="Javascript">
```js
const crypto = require('crypto');
const axios = require('axios');
const BASE_URL = 'https://app.infisical.com';
const ALGORITHM = 'aes-256-gcm';
const decrypt = ({ ciphertext, iv, tag, secret}) => {
const decipher = crypto.createDecipheriv(
ALGORITHM,
secret,
Buffer.from(iv, 'base64')
);
decipher.setAuthTag(Buffer.from(tag, 'base64'));
let cleartext = decipher.update(ciphertext, 'base64', 'utf8');
cleartext += decipher.final('utf8');
return cleartext;
}
const getSecret = async () => {
const serviceToken = 'your_service_token';
const serviceTokenSecret = serviceToken.substring(serviceToken.lastIndexOf('.') + 1);
const secretType = 'shared' // 'shared' or 'personal'
const secretKey = 'some_key';
// 1. Get your Infisical Token data
const { data: serviceTokenData } = await axios.get(
`${BASE_URL}/api/v2/service-token`,
{
headers: {
Authorization: `Bearer ${serviceToken}`
}
}
);
// 2. Get the secret from your project and environment
const { data } = await axios.get(
`${BASE_URL}/api/v3/secrets/${secretKey}?${new URLSearchParams({
environment: serviceTokenData.environment,
workspaceId: serviceTokenData.workspace,
type: secretType // optional, defaults to 'shared'
})}`,
{
headers: {
Authorization: `Bearer ${serviceToken}`
}
}
);
const encryptedSecret = data.secret;
// 3. Decrypt the (encrypted) project key with the key from your Infisical Token
const projectKey = decrypt({
ciphertext: serviceTokenData.encryptedKey,
iv: serviceTokenData.iv,
tag: serviceTokenData.tag,
secret: serviceTokenSecret
});
// 4. Decrypt the (encrypted) secret value
const secretValue = decrypt({
ciphertext: encryptedSecret.secretValueCiphertext,
iv: encryptedSecret.secretValueIV,
tag: encryptedSecret.secretValueTag,
secret: projectKey
});
console.log('secret: ', ({
secretKey,
secretValue
}));
}
getSecret();
```
</Tab>
<Tab title="Python">
```Python
import requests
import base64
from Cryptodome.Cipher import AES
BASE_URL = "http://app.infisical.com"
def decrypt(ciphertext, iv, tag, secret):
secret = bytes(secret, "utf-8")
iv = base64.standard_b64decode(iv)
tag = base64.standard_b64decode(tag)
ciphertext = base64.standard_b64decode(ciphertext)
cipher = AES.new(secret, AES.MODE_GCM, iv)
cipher.update(tag)
cleartext = cipher.decrypt(ciphertext).decode("utf-8")
return cleartext
def get_secret():
service_token = "your_service_token"
service_token_secret = service_token[service_token.rindex(".") + 1 :]
secret_type = "shared" # "shared" or "personal"
secret_key = "some_key"
# 1. Get your Infisical Token data
service_token_data = requests.get(
f"{BASE_URL}/api/v2/service-token",
headers={"Authorization": f"Bearer {service_token}"},
).json()
# 2. Get secret from your project and environment
data = requests.get(
f"{BASE_URL}/api/v3/secrets/{secret_key}",
params={
"environment": service_token_data["environment"],
"workspaceId": service_token_data["workspace"],
"type": secret_type # optional, defaults to "shared"
},
headers={"Authorization": f"Bearer {service_token}"},
).json()
encrypted_secret = data["secret"]
# 3. Decrypt the (encrypted) project key with the key from your Infisical Token
project_key = decrypt(
ciphertext=service_token_data["encryptedKey"],
iv=service_token_data["iv"],
tag=service_token_data["tag"],
secret=service_token_secret,
)
# 4. Decrypt the (encrypted) secret value
secret_value = decrypt(
ciphertext=encrypted_secret["secretValueCiphertext"],
iv=encrypted_secret["secretValueIV"],
tag=encrypted_secret["secretValueTag"],
secret=project_key,
)
print("secret: ", {
"secret_key": secret_key,
"secret_value": secret_value
})
get_secret()
```
</Tab>
</Tabs>

View File

@@ -1,195 +0,0 @@
---
title: "Retrieve secrets"
description: "How to get all secrets using an Infisical Token scoped to a project and environment"
---
Prerequisites:
- Set up and add envars to [Infisical Cloud](https://app.infisical.com).
- Create an [Infisical Token](/documentation/platform/token) for your project and environment.
- Grasp a basic understanding of the system and its underlying cryptography [here](/api-reference/overview/introduction).
- [Ensure that your project is blind-indexed](../blind-indices).
## Flow
1. [Get your Infisical Token data](/api-reference/endpoints/service-tokens/get) including a (encrypted) project key.
2. [Get secrets for your project and environment](/api-reference/endpoints/secrets/read).
3. Decrypt the (encrypted) project key with the key from your Infisical Token.
4. Decrypt the (encrypted) secrets
## Example
<Tabs>
<Tab title="Javascript">
```js
const crypto = require('crypto');
const axios = require('axios');
const BASE_URL = 'https://app.infisical.com';
const ALGORITHM = 'aes-256-gcm';
const decrypt = ({ ciphertext, iv, tag, secret}) => {
const decipher = crypto.createDecipheriv(
ALGORITHM,
secret,
Buffer.from(iv, 'base64')
);
decipher.setAuthTag(Buffer.from(tag, 'base64'));
let cleartext = decipher.update(ciphertext, 'base64', 'utf8');
cleartext += decipher.final('utf8');
return cleartext;
}
const getSecrets = async () => {
const serviceToken = 'your_service_token';
const serviceTokenSecret = serviceToken.substring(serviceToken.lastIndexOf('.') + 1);
// 1. Get your Infisical Token data
const { data: serviceTokenData } = await axios.get(
`${BASE_URL}/api/v2/service-token`,
{
headers: {
Authorization: `Bearer ${serviceToken}`
}
}
);
// 2. Get secrets for your project and environment
const { data } = await axios.get(
`${BASE_URL}/api/v3/secrets?${new URLSearchParams({
environment: serviceTokenData.environment,
workspaceId: serviceTokenData.workspace
})}`,
{
headers: {
Authorization: `Bearer ${serviceToken}`
}
}
);
const encryptedSecrets = data.secrets;
// 3. Decrypt the (encrypted) project key with the key from your Infisical Token
const projectKey = decrypt({
ciphertext: serviceTokenData.encryptedKey,
iv: serviceTokenData.iv,
tag: serviceTokenData.tag,
secret: serviceTokenSecret
});
// 4. Decrypt the (encrypted) secrets
const secrets = encryptedSecrets.map((secret) => {
const secretKey = decrypt({
ciphertext: secret.secretKeyCiphertext,
iv: secret.secretKeyIV,
tag: secret.secretKeyTag,
secret: projectKey
});
const secretValue = decrypt({
ciphertext: secret.secretValueCiphertext,
iv: secret.secretValueIV,
tag: secret.secretValueTag,
secret: projectKey
});
return ({
secretKey,
secretValue
});
});
console.log('secrets: ', secrets);
}
getSecrets();
```
</Tab>
<Tab title="Python">
```Python
import requests
import base64
from Cryptodome.Cipher import AES
BASE_URL = "http://app.infisical.com"
def decrypt(ciphertext, iv, tag, secret):
secret = bytes(secret, "utf-8")
iv = base64.standard_b64decode(iv)
tag = base64.standard_b64decode(tag)
ciphertext = base64.standard_b64decode(ciphertext)
cipher = AES.new(secret, AES.MODE_GCM, iv)
cipher.update(tag)
cleartext = cipher.decrypt(ciphertext).decode("utf-8")
return cleartext
def get_secrets():
service_token = "your_service_token"
service_token_secret = service_token[service_token.rindex(".") + 1 :]
# 1. Get your Infisical Token data
service_token_data = requests.get(
f"{BASE_URL}/api/v2/service-token",
headers={"Authorization": f"Bearer {service_token}"},
).json()
# 2. Get secrets for your project and environment
data = requests.get(
f"{BASE_URL}/api/v3/secrets",
params={
"environment": service_token_data["environment"],
"workspaceId": service_token_data["workspace"],
},
headers={"Authorization": f"Bearer {service_token}"},
).json()
encrypted_secrets = data["secrets"]
# 3. Decrypt the (encrypted) project key with the key from your Infisical Token
project_key = decrypt(
ciphertext=service_token_data["encryptedKey"],
iv=service_token_data["iv"],
tag=service_token_data["tag"],
secret=service_token_secret,
)
# 4. Decrypt the (encrypted) secrets
secrets = []
for secret in encrypted_secrets:
secret_key = decrypt(
ciphertext=secret["secretKeyCiphertext"],
iv=secret["secretKeyIV"],
tag=secret["secretKeyTag"],
secret=project_key,
)
secret_value = decrypt(
ciphertext=secret["secretValueCiphertext"],
iv=secret["secretValueIV"],
tag=secret["secretValueTag"],
secret=project_key,
)
secrets.append(
{
"secret_key": secret_key,
"secret_value": secret_value,
}
)
print("secrets:", secrets)
get_secrets()
```
</Tab>
</Tabs>

View File

@@ -1,229 +0,0 @@
---
title: "Update secret"
description: "How to update a secret using an Infisical Token scoped to a project and environment"
---
Prerequisites:
- Set up and add envars to [Infisical Cloud](https://app.infisical.com).
- Create an [Infisical Token](/documentation/platform/token) for your project and environment with write access enabled.
- Grasp a basic understanding of the system and its underlying cryptography [here](/api-reference/overview/introduction).
- [Ensure that your project is blind-indexed](../blind-indices).
## Flow
1. [Get your Infisical Token data](/api-reference/endpoints/service-tokens/get) including a (encrypted) project key.
2. Decrypt the (encrypted) project key with the key from your Infisical Token.
3. Encrypt your updated secret with the project key
4. [Send (encrypted) updated secret to Infical](/api-reference/endpoints/secrets/update)
## Example
<Tabs>
<Tab title="Javascript">
```js
const crypto = require('crypto');
const axios = require('axios');
const BASE_URL = 'https://app.infisical.com';
const ALGORITHM = 'aes-256-gcm';
const BLOCK_SIZE_BYTES = 16;
const encrypt = ({ text, secret }) => {
const iv = crypto.randomBytes(BLOCK_SIZE_BYTES);
const cipher = crypto.createCipheriv(ALGORITHM, secret, iv);
let ciphertext = cipher.update(text, 'utf8', 'base64');
ciphertext += cipher.final('base64');
return {
ciphertext,
iv: iv.toString('base64'),
tag: cipher.getAuthTag().toString('base64')
};
}
const decrypt = ({ ciphertext, iv, tag, secret}) => {
const decipher = crypto.createDecipheriv(
ALGORITHM,
secret,
Buffer.from(iv, 'base64')
);
decipher.setAuthTag(Buffer.from(tag, 'base64'));
let cleartext = decipher.update(ciphertext, 'base64', 'utf8');
cleartext += decipher.final('utf8');
return cleartext;
}
const updateSecrets = async () => {
const serviceToken = 'your_service_token';
const serviceTokenSecret = serviceToken.substring(serviceToken.lastIndexOf('.') + 1);
const secretType = 'shared' // 'shared' or 'personal'
const secretKey = 'some_key';
const secretValue = 'updated_value';
const secretComment = 'updated_comment';
// 1. Get your Infisical Token data
const { data: serviceTokenData } = await axios.get(
`${BASE_URL}/api/v2/service-token`,
{
headers: {
Authorization: `Bearer ${serviceToken}`
}
}
);
// 2. Decrypt the (encrypted) project key with the key from your Infisical Token
const projectKey = decrypt({
ciphertext: serviceTokenData.encryptedKey,
iv: serviceTokenData.iv,
tag: serviceTokenData.tag,
secret: serviceTokenSecret
});
// 3. Encrypt your updated secret with the project key
const {
ciphertext: secretKeyCiphertext,
iv: secretKeyIV,
tag: secretKeyTag
} = encrypt({
text: secretKey,
secret: projectKey
});
const {
ciphertext: secretValueCiphertext,
iv: secretValueIV,
tag: secretValueTag
} = encrypt({
text: secretValue,
secret: projectKey
});
const {
ciphertext: secretCommentCiphertext,
iv: secretCommentIV,
tag: secretCommentTag
} = encrypt({
text: secretComment,
secret: projectKey
});
// 4. Send (encrypted) updated secret to Infisical
await axios.patch(
`${BASE_URL}/api/v3/secrets/${secretKey}`,
{
workspaceId: serviceTokenData.workspace,
environment: serviceTokenData.environment,
type: secretType,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag
},
{
headers: {
Authorization: `Bearer ${serviceToken}`
}
}
);
}
updateSecrets();
```
</Tab>
<Tab title="Python">
```Python
import base64
import requests
from Cryptodome.Cipher import AES
from Cryptodome.Random import get_random_bytes
BASE_URL = "https://app.infisical.com"
BLOCK_SIZE_BYTES = 16
def encrypt(text, secret):
iv = get_random_bytes(BLOCK_SIZE_BYTES)
secret = bytes(secret, "utf-8")
cipher = AES.new(secret, AES.MODE_GCM, iv)
ciphertext, tag = cipher.encrypt_and_digest(text.encode("utf-8"))
return {
"ciphertext": base64.standard_b64encode(ciphertext).decode("utf-8"),
"tag": base64.standard_b64encode(tag).decode("utf-8"),
"iv": base64.standard_b64encode(iv).decode("utf-8"),
}
def decrypt(ciphertext, iv, tag, secret):
secret = bytes(secret, "utf-8")
iv = base64.standard_b64decode(iv)
tag = base64.standard_b64decode(tag)
ciphertext = base64.standard_b64decode(ciphertext)
cipher = AES.new(secret, AES.MODE_GCM, iv)
cipher.update(tag)
cleartext = cipher.decrypt(ciphertext).decode("utf-8")
return cleartext
def update_secret():
service_token = "your_service_token"
service_token_secret = service_token[service_token.rindex(".") + 1 :]
secret_type = "shared" # "shared" or "personal"
secret_key = "some_key"
secret_value = "updated_value"
secret_comment = "updated_comment"
# 1. Get your Infisical Token data
service_token_data = requests.get(
f"{BASE_URL}/api/v2/service-token",
headers={"Authorization": f"Bearer {service_token}"},
).json()
# 2. Decrypt the (encrypted) project key with the key from your Infisical Token
project_key = decrypt(
ciphertext=service_token_data["encryptedKey"],
iv=service_token_data["iv"],
tag=service_token_data["tag"],
secret=service_token_secret,
)
# 3. Encrypt your updated secret with the project key
encrypted_key_data = encrypt(text=secret_key, secret=project_key)
encrypted_value_data = encrypt(text=secret_value, secret=project_key)
encrypted_comment_data = encrypt(text=secret_comment, secret=project_key)
# 4. Send (encrypted) updated secret to Infisical
requests.patch(
f"{BASE_URL}/api/v3/secrets/{secret_key}",
json={
"workspaceId": service_token_data["workspace"],
"environment": service_token_data["environment"],
"type": secret_type,
"secretKeyCiphertext": encrypted_key_data["ciphertext"],
"secretKeyIV": encrypted_key_data["iv"],
"secretKeyTag": encrypted_key_data["tag"],
"secretValueCiphertext": encrypted_value_data["ciphertext"],
"secretValueIV": encrypted_value_data["iv"],
"secretValueTag": encrypted_value_data["tag"],
"secretCommentCiphertext": encrypted_comment_data["ciphertext"],
"secretCommentIV": encrypted_comment_data["iv"],
"secretCommentTag": encrypted_comment_data["tag"]
},
headers={"Authorization": f"Bearer {service_token}"},
)
update_secret()
```
</Tab>
</Tabs>

View File

@@ -0,0 +1,59 @@
---
title: "REST API"
---
Infisical's Public (REST) API is the most flexible, platform-agnostic way to read/write secrets for your application.
Prerequisites:
- Have a project with secrets ready in [Infisical Cloud](https://app.infisical.com).
- Create an [Infisical Token](/documentation/platform/token) scoped to an environment in your project in Infisical.
To keep it simple, we're going to fetch secrets from the API with **End-to-End Encryption (E2EE)** disabled.
<Note>
It's possible to use the API with **E2EE** enabled but this means learning about how encryption works with Infisical and performing client-side encryption/decryption operations yourself.
yourself.
If **E2EE** is a must for your team, we recommend either using one of the [Infisical SDKs](/documentation/getting-started/sdks) or checking out the [examples for E2EE](/api-reference/overview/examples/e2ee-disabled).
</Note>
## Configuration
Head to your Project Settings, where you created your service token, and un-check the **E2EE** setting.
## Retrieve Secret
Retrieve a secret from the project and environment in Infisical scoped to your service token by making a HTTP request with the following format/details:
```bash
curl --location --request GET 'https://app.infisical.com/api/v3/secrets/raw/secretName?workspaceId=workspaceId&environment=environment' \
--header 'Authorization: Bearer serviceToken'
```
<ParamField path="secretName" type="string" required>
Name of secret to retrieve
</ParamField>
<ParamField query="workspaceId" type="string" required>
The ID of the workspace
</ParamField>
<ParamField query="environment" type="string" required>
The environment slug
</ParamField>
<ParamField query="secretPath" type="string" default="/" optional>
Path to secrets in workspace
</ParamField>
<ParamField query="type" type="string" optional default="personal">
The type of the secret. Valid options are “shared” or “personal”
</ParamField>
Depending on your application requirements, you may wish to use Infisical's API in different ways such as by retaining **E2EE**
or fetching multiple secrets at once instead of one at a time.
Whatever the case, we recommend glossing over the [API Examples](/api-reference/overview/examples/note)
to gain a deeper understanding of how you to best leverage the Infisical API for your use-case.
See also:
- Explore the [API Examples](/api-reference/overview/examples/note)
- [API Reference](/api-reference/overview/introduction)

View File

@@ -42,6 +42,14 @@ Start syncing environment variables with [Infisical Cloud](https://app.infisical
> >
Fetch and save secrets as native Kubernetes secrets Fetch and save secrets as native Kubernetes secrets
</Card> </Card>
<Card
href="/documentation/getting-started/api"
title="REST API"
icon="cloud"
color="#3775a9"
>
Fetch secrets via HTTP request
</Card>
</CardGroup> </CardGroup>
## Resources ## Resources

View File

@@ -1,34 +1,91 @@
--- ---
title: "Terraform" title: "Terraform"
description: "How to use Infisical to inject environment variables and secrets into terraform." description: "Fetch Secrets From Infisical With Terraform"
--- ---
Prerequisites: This guide provides step-by-step guidance on how to fetch secrets from Infisical using Terraform.
- Set up and add envars to [Infisical Cloud](https://app.infisical.com) ## Prerequisites
- [Install the CLI](/cli/overview)
## Initialize Infisical for your [Terraform](https://www.terraform.io/) project - Basic understanding of Terraform
- Install [Terraform](https://www.terraform.io/downloads.html)
```bash ## Steps
# navigate to the root of your of your project
cd /path/to/project
# then initialize Infisical ### 1. Define Required Providers
infisical init
Specify `infisical` in the `required_providers` block within the `terraform` block of your configuration file. If you would like to use a specific version of the provider, uncomment and replace `<latest version>` with the version of the Infisical provider that you want to use.
```hcl main.tf
terraform {
required_providers {
infisical = {
# version = <latest version>
source = "infisical/infisical"
}
}
}
``` ```
## Run terraform as usual but with Infisical ### 2. Configure the Infisical Provider
```bash Set up the Infisical provider by specifying the `host` and `service_token`. Replace `<>` in `service_token` with your actual token. The `host` is only required if you are using a self-hosted instance of Infisical.
infisical run -- <your application start command>
# Example ```hcl main.tf
infisical run -- terraform plan provider "infisical" {
host = "https://app.infisical.com" # Only required if using self hosted instance of Infisical, default is https://app.infisical.com
service_token = "<>" # Get token https://infisical.com/docs/documentation/platform/token
}
``` ```
<Note> <Warning>
To inject any arbitrary variable to terraform, you have It is recommended to use Terraform variables to pass your service token dynamically to avoid hard coding it
to prefix them with `TF_VAR`. Read more about that </Warning>
[here](https://developer.hashicorp.com/terraform/cli/config/environment-variables#tf_var_name).
</Note> ### 3. Fetch Infisical Secrets
Use the `infisical_secrets` data source to fetch your secrets. This is defined with an empty block `{}` as the provider automatically fetches all secrets associated with your service token.
```hcl main.tf
data "infisical_secrets" "my-secrets" {}
```
### 4. Define Outputs
As an example, we are going to output your fetched secrets. Replace `SECRET-NAME` with the actual name of your secret.
For a single secret:
```hcl main.tf
output "single-secret" {
value = data.infisical_secrets.my-secrets.secrets["SECRET-NAME"]
}
```
For all secrets:
```hcl
output "all-secrets" {
value = data.infisical_secrets.my-secrets.secrets
}
```
### 5. Run Terraform
Once your configuration is complete, initialize your Terraform working directory:
```bash
$ terraform init
```
Then, run the plan command to view the fetched secrets:
```bash
$ terraform plan
```
Terraform will now fetch your secrets from Infisical and display them as output according to your configuration.
## Conclusion
You have now successfully set up and used the Infisical provider with Terraform to fetch secrets. For more information, visit the [Infisical documentation](https://registry.terraform.io/providers/Infisical/infisical/latest/docs).

View File

@@ -92,7 +92,8 @@
"documentation/getting-started/sdks", "documentation/getting-started/sdks",
"documentation/getting-started/cli", "documentation/getting-started/cli",
"documentation/getting-started/docker", "documentation/getting-started/docker",
"documentation/getting-started/kubernetes" "documentation/getting-started/kubernetes",
"documentation/getting-started/api"
] ]
}, },
{ {
@@ -250,9 +251,9 @@
{ {
"group": "Examples", "group": "Examples",
"pages": [ "pages": [
"api-reference/overview/encryption-modes/overview", "api-reference/overview/examples/note",
"api-reference/overview/encryption-modes/es-mode", "api-reference/overview/examples/e2ee-disabled",
"api-reference/overview/encryption-modes/e2ee-mode" "api-reference/overview/examples/e2ee-enabled"
] ]
}, },
"api-reference/overview/blind-indices" "api-reference/overview/blind-indices"

View File

@@ -47,8 +47,6 @@ paths:
description: Secret versions description: Secret versions
'400': '400':
description: Bad Request description: Bad Request
security:
- apiKeyAuth: []
/api/v1/secret/{secretId}/secret-versions/rollback: /api/v1/secret/{secretId}/secret-versions/rollback:
post: post:
summary: Roll back secret to a version. summary: Roll back secret to a version.
@@ -74,8 +72,6 @@ paths:
description: Secret rolled back to description: Secret rolled back to
'400': '400':
description: Bad Request description: Bad Request
security:
- apiKeyAuth: []
requestBody: requestBody:
required: true required: true
content: content:
@@ -138,8 +134,6 @@ paths:
description: Project secret snapshots description: Project secret snapshots
'400': '400':
description: Bad Request description: Bad Request
security:
- apiKeyAuth: []
/api/v1/workspace/{workspaceId}/secret-snapshots/count: /api/v1/workspace/{workspaceId}/secret-snapshots/count:
get: get:
description: '' description: ''
@@ -184,8 +178,6 @@ paths:
description: Secrets rolled back to description: Secrets rolled back to
'400': '400':
description: Bad Request description: Bad Request
security:
- apiKeyAuth: []
requestBody: requestBody:
required: true required: true
content: content:
@@ -255,8 +247,6 @@ paths:
description: Project logs description: Project logs
'400': '400':
description: Bad Request description: Bad Request
security:
- apiKeyAuth: []
/api/v1/action/{actionId}: /api/v1/action/{actionId}:
get: get:
description: '' description: ''
@@ -1677,8 +1667,6 @@ paths:
description: Current user on request description: Current user on request
'400': '400':
description: Bad Request description: Bad Request
security:
- apiKeyAuth: []
/api/v2/users/me/mfa: /api/v2/users/me/mfa:
patch: patch:
description: '' description: ''
@@ -1716,8 +1704,6 @@ paths:
description: Organizations that user is part of description: Organizations that user is part of
'400': '400':
description: Bad Request description: Bad Request
security:
- apiKeyAuth: []
/api/v2/organizations/{organizationId}/memberships: /api/v2/organizations/{organizationId}/memberships:
get: get:
summary: Return organization memberships summary: Return organization memberships
@@ -1744,8 +1730,6 @@ paths:
description: Memberships of organization description: Memberships of organization
'400': '400':
description: Bad Request description: Bad Request
security:
- apiKeyAuth: []
/api/v2/organizations/{organizationId}/memberships/{membershipId}: /api/v2/organizations/{organizationId}/memberships/{membershipId}:
patch: patch:
summary: Update organization membership summary: Update organization membership
@@ -1776,8 +1760,6 @@ paths:
description: Updated organization membership description: Updated organization membership
'400': '400':
description: Bad Request description: Bad Request
security:
- apiKeyAuth: []
requestBody: requestBody:
required: true required: true
content: content:
@@ -1819,8 +1801,6 @@ paths:
description: Deleted organization membership description: Deleted organization membership
'400': '400':
description: Bad Request description: Bad Request
security:
- apiKeyAuth: []
/api/v2/organizations/{organizationId}/workspaces: /api/v2/organizations/{organizationId}/workspaces:
get: get:
summary: Return projects in organization that user is part of summary: Return projects in organization that user is part of
@@ -1845,8 +1825,6 @@ paths:
items: items:
$ref: '#/components/schemas/Project' $ref: '#/components/schemas/Project'
description: Projects of organization description: Projects of organization
security:
- apiKeyAuth: []
/api/v2/organizations/{organizationId}/service-accounts: /api/v2/organizations/{organizationId}/service-accounts:
get: get:
description: '' description: ''
@@ -2057,8 +2035,6 @@ paths:
description: Encrypted project key for the given project description: Encrypted project key for the given project
'400': '400':
description: Bad Request description: Bad Request
security:
- apiKeyAuth: []
/api/v2/workspace/{workspaceId}/service-token-data: /api/v2/workspace/{workspaceId}/service-token-data:
get: get:
description: '' description: ''
@@ -2099,8 +2075,6 @@ paths:
description: Memberships of project description: Memberships of project
'400': '400':
description: Bad Request description: Bad Request
security:
- apiKeyAuth: []
/api/v2/workspace/{workspaceId}/memberships/{membershipId}: /api/v2/workspace/{workspaceId}/memberships/{membershipId}:
patch: patch:
summary: Update project membership summary: Update project membership
@@ -2131,8 +2105,6 @@ paths:
description: Updated membership description: Updated membership
'400': '400':
description: Bad Request description: Bad Request
security:
- apiKeyAuth: []
requestBody: requestBody:
required: true required: true
content: content:
@@ -2172,8 +2144,6 @@ paths:
description: Deleted membership description: Deleted membership
'400': '400':
description: Bad Request description: Bad Request
security:
- apiKeyAuth: []
/api/v2/workspace/{workspaceId}/auto-capitalization: /api/v2/workspace/{workspaceId}/auto-capitalization:
patch: patch:
description: '' description: ''
@@ -2407,8 +2377,6 @@ paths:
description: >- description: >-
Newly-created secrets for the given project and Newly-created secrets for the given project and
environment environment
security:
- apiKeyAuth: []
requestBody: requestBody:
required: true required: true
content: content:
@@ -2462,8 +2430,6 @@ paths:
items: items:
$ref: '#/components/schemas/Secret' $ref: '#/components/schemas/Secret'
description: Secrets for the given project and environment description: Secrets for the given project and environment
security:
- apiKeyAuth: []
patch: patch:
summary: Update secret(s) summary: Update secret(s)
description: Update secret(s) description: Update secret(s)
@@ -2481,8 +2447,6 @@ paths:
items: items:
$ref: '#/components/schemas/Secret' $ref: '#/components/schemas/Secret'
description: Updated secrets description: Updated secrets
security:
- apiKeyAuth: []
requestBody: requestBody:
required: true required: true
content: content:
@@ -2514,8 +2478,6 @@ paths:
items: items:
$ref: '#/components/schemas/Secret' $ref: '#/components/schemas/Secret'
description: Deleted secrets description: Deleted secrets
security:
- apiKeyAuth: []
requestBody: requestBody:
required: true required: true
content: content:

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

View File

@@ -17,6 +17,12 @@
"image": "Kubernetes", "image": "Kubernetes",
"docsLink": "https://infisical.com/docs/integrations/platforms/kubernetes" "docsLink": "https://infisical.com/docs/integrations/platforms/kubernetes"
}, },
{
"name": "Terraform",
"slug": "terraform",
"image": "Terraform",
"docsLink": "https://infisical.com/docs/integrations/frameworks/terraform"
},
{ {
"name": "React", "name": "React",
"slug": "react", "slug": "react",

View File

@@ -4,7 +4,11 @@ import { useRouter } from 'next/router';
import { faArrowRight, faCheck, faXmark } from '@fortawesome/free-solid-svg-icons'; import { faArrowRight, faCheck, faXmark } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
// TODO: This needs to be moved from public folder // TODO: This needs to be moved from public folder
import { contextNetlifyMapping, integrationSlugNameMapping, reverseContextNetlifyMapping } from 'public/data/frequentConstants'; import {
contextNetlifyMapping,
integrationSlugNameMapping,
reverseContextNetlifyMapping
} from 'public/data/frequentConstants';
import Button from '@app/components/basic/buttons/Button'; import Button from '@app/components/basic/buttons/Button';
import ListBox from '@app/components/basic/Listbox'; import ListBox from '@app/components/basic/Listbox';
@@ -27,6 +31,7 @@ interface Integration {
targetEnvironment: string; targetEnvironment: string;
workspace: string; workspace: string;
integrationAuth: string; integrationAuth: string;
secretPath: string;
} }
interface IntegrationApp { interface IntegrationApp {
@@ -55,7 +60,6 @@ const IntegrationTile = ({
environments = [], environments = [],
handleDeleteIntegration handleDeleteIntegration
}: Props) => { }: Props) => {
const [integrationEnvironment, setIntegrationEnvironment] = useState<Props['environments'][0]>( const [integrationEnvironment, setIntegrationEnvironment] = useState<Props['environments'][0]>(
environments.find(({ slug }) => slug === integration?.environment) || { environments.find(({ slug }) => slug === integration?.environment) || {
name: '', name: '',
@@ -74,16 +78,16 @@ const IntegrationTile = ({
}); });
setApps(tempApps); setApps(tempApps);
if (integration?.app) { if (integration?.app) {
setIntegrationApp(integration.app); setIntegrationApp(integration.app);
} else if (integration?.path && integration?.region) { } else if (integration?.path && integration?.region) {
setIntegrationApp(`${integration.path} (${integration.region})`); setIntegrationApp(`${integration.path} (${integration.region})`);
} else if (tempApps.length > 0) { } else if (tempApps.length > 0) {
setIntegrationApp(tempApps[0].name) setIntegrationApp(tempApps[0].name);
} else { } else {
setIntegrationApp(''); setIntegrationApp('');
} }
switch (integration.integration) { switch (integration.integration) {
case 'vercel': case 'vercel':
@@ -212,12 +216,15 @@ const IntegrationTile = ({
return <div />; return <div />;
}; };
if (!integrationApp && integration.integration !== "checkly") return <div />; if (!integrationApp && integration.integration !== 'checkly') return <div />;
const isSelected = integration.integration === 'hashicorp-vault' ? `${integration.app} - path: ${integration.path}` : integrationApp; const isSelected =
integration.integration === 'hashicorp-vault'
? `${integration.app} - path: ${integration.path}`
: integrationApp;
return ( return (
<div className="mx-6 mb-8 flex max-w-5xl justify-between rounded-md bg-mineshaft-800 border border-mineshaft-600 p-6"> <div className="mx-6 mb-8 flex max-w-5xl justify-between rounded-md border border-mineshaft-600 bg-mineshaft-800 p-6">
<div className="flex"> <div className="flex">
<div> <div>
<p className="mb-2 text-xs font-semibold text-gray-400">ENVIRONMENT</p> <p className="mb-2 text-xs font-semibold text-gray-400">ENVIRONMENT</p>
@@ -235,33 +242,46 @@ const IntegrationTile = ({
isFull isFull
/> />
</div> </div>
<div className="ml-2">
<p className="mb-2 text-xs font-semibold text-gray-400">SECRET PATH</p>
<div className="cursor-default rounded-md bg-white/[.07] py-2.5 pl-4 pr-10 text-sm font-semibold text-gray-300">
{/* {integration.integration.charAt(0).toUpperCase() + integration.integration.slice(1)} */}
{integration.secretPath}
</div>
</div>
<div className="pt-2"> <div className="pt-2">
<FontAwesomeIcon icon={faArrowRight} className="mx-4 mt-8 text-gray-400" /> <FontAwesomeIcon icon={faArrowRight} className="mx-4 mt-8 text-gray-400" />
</div> </div>
<div className="mr-2"> <div className="mr-2">
<p className="text-gray-400 text-xs font-semibold mb-2">INTEGRATION</p> <p className="mb-2 text-xs font-semibold text-gray-400">INTEGRATION</p>
<div className="py-2.5 bg-white/[.07] rounded-md pl-4 pr-10 text-sm font-semibold text-gray-300 cursor-default"> <div className="cursor-default rounded-md bg-white/[.07] py-2.5 pl-4 pr-10 text-sm font-semibold text-gray-300">
{/* {integration.integration.charAt(0).toUpperCase() + integration.integration.slice(1)} */} {/* {integration.integration.charAt(0).toUpperCase() + integration.integration.slice(1)} */}
{integrationSlugNameMapping[integration.integration]} {integrationSlugNameMapping[integration.integration]}
</div> </div>
</div> </div>
<div className="mr-2"> <div className="mr-2">
<div className="mb-2 text-xs font-semibold text-gray-400">APP</div> <div className="mb-2 text-xs font-semibold text-gray-400">APP</div>
{integrationApp ? <div title={integrationApp}> {integrationApp ? (
<ListBox <div title={integrationApp}>
data={!integration.isActive ? apps.map((app) => app.name) : null} <ListBox
isSelected={isSelected} data={!integration.isActive ? apps.map((app) => app.name) : null}
onChange={(app) => { isSelected={isSelected}
setIntegrationApp(app); onChange={(app) => {
}} setIntegrationApp(app);
/> }}
</div> : <div className='w-52 h-10 rounded-md bg-mineshaft-600 animate-pulse px-4 font-bold py-2'>-</div>} />
</div>
) : (
<div className="h-10 w-52 animate-pulse rounded-md bg-mineshaft-600 px-4 py-2 font-bold">
-
</div>
)}
</div> </div>
{renderIntegrationSpecificParams(integration)} {renderIntegrationSpecificParams(integration)}
</div> </div>
<div className="flex items-end cursor-default"> <div className="flex cursor-default items-end">
{integration.isActive ? ( {integration.isActive ? (
<div className="flex max-w-5xl flex-row items-center rounded-md bg-mineshaft-600 p-[0.44rem] px-4 border border-mineshaft-500"> <div className="flex max-w-5xl flex-row items-center rounded-md border border-mineshaft-500 bg-mineshaft-600 p-[0.44rem] px-4">
<FontAwesomeIcon icon={faCheck} className="mr-2.5 text-lg text-primary" /> <FontAwesomeIcon icon={faCheck} className="mr-2.5 text-lg text-primary" />
<div className="font-semibold text-gray-300">In Sync</div> <div className="font-semibold text-gray-300">In Sync</div>
</div> </div>

View File

@@ -23,6 +23,7 @@ interface Integration {
targetEnvironment: string; targetEnvironment: string;
workspace: string; workspace: string;
integrationAuth: string; integrationAuth: string;
secretPath: string;
} }
const ProjectIntegrationSection = ({ const ProjectIntegrationSection = ({

View File

@@ -76,7 +76,7 @@ export default function InitialLoginStep({
{t('login.continue-with-google')} {t('login.continue-with-google')}
</Button> </Button>
</div> */} </div> */}
<div className="relative md:px-1.5 flex items-center justify-center lg:w-1/6 w-1/4 min-w-[20rem] md:min-w-[22rem] mx-auto w-full rounded-lg max-h-24 md:max-h-28"> <div className="relative md:px-1.5 flex items-center justify-center lg:w-1/6 w-1/4 min-w-[21.3rem] md:min-w-[22rem] mx-auto rounded-lg max-h-24 md:max-h-28">
<div className="flex items-center justify-center w-full md:px-2 md:py-1 rounded-lg max-h-24 md:max-h-28"> <div className="flex items-center justify-center w-full md:px-2 md:py-1 rounded-lg max-h-24 md:max-h-28">
<Input <Input
value={email} value={email}
@@ -89,7 +89,7 @@ export default function InitialLoginStep({
/> />
</div> </div>
</div> </div>
<div className="relative pt-2 md:pt-0 md:px-1.5 flex items-center justify-center lg:w-1/6 w-1/4 min-w-[20rem] md:min-w-[22rem] mx-auto w-full rounded-lg max-h-24 md:max-h-28"> <div className="relative pt-2 md:pt-0 md:px-1.5 flex items-center justify-center w-1/4 lg:w-1/6 min-w-[21.3rem] md:min-w-[22rem] mx-auto rounded-lg max-h-24 md:max-h-28">
<div className="flex items-center justify-center w-full md:p-2 rounded-lg max-h-24 md:max-h-28"> <div className="flex items-center justify-center w-full md:p-2 rounded-lg max-h-24 md:max-h-28">
<Input <Input
value={password} value={password}
@@ -137,5 +137,11 @@ export default function InitialLoginStep({
<span className='hover:underline hover:underline-offset-4 hover:decoration-primary-700 hover:text-bunker-200 duration-200 cursor-pointer'>{t('login.create-account')}</span> <span className='hover:underline hover:underline-offset-4 hover:decoration-primary-700 hover:text-bunker-200 duration-200 cursor-pointer'>{t('login.create-account')}</span>
</Link> </Link>
</div> </div>
<div className="text-bunker-400 text-sm flex flex-row">
<span className="mr-1">Forgot password?</span>
<Link href="/verify-email">
<span className='hover:underline hover:underline-offset-4 hover:decoration-primary-700 hover:text-bunker-200 duration-200 cursor-pointer'>Recover your account</span>
</Link>
</div>
</div> </div>
} }

View File

@@ -1 +1,7 @@
export { useCreateFolder, useDeleteFolder, useGetProjectFolders, useUpdateFolder } from './queries'; export {
useCreateFolder,
useDeleteFolder,
useGetProjectFolders,
useGetProjectFoldersBatch,
useUpdateFolder
} from './queries';

View File

@@ -1,5 +1,5 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueries, useQuery, useQueryClient } from '@tanstack/react-query';
import { apiRequest } from '@app/config/request'; import { apiRequest } from '@app/config/request';
@@ -7,6 +7,7 @@ import { secretSnapshotKeys } from '../secretSnapshots/queries';
import { import {
CreateFolderDTO, CreateFolderDTO,
DeleteFolderDTO, DeleteFolderDTO,
GetProjectFoldersBatchDTO,
GetProjectFoldersDTO, GetProjectFoldersDTO,
TSecretFolder, TSecretFolder,
UpdateFolderDTO UpdateFolderDTO
@@ -17,6 +18,26 @@ const queryKeys = {
['secret-folders', { workspaceId, environment, parentFolderId }] as const ['secret-folders', { workspaceId, environment, parentFolderId }] as const
}; };
const fetchProjectFolders = async (
workspaceId: string,
environment: string,
parentFolderId?: string,
parentFolderPath?: string
) => {
const { data } = await apiRequest.get<{ folders: TSecretFolder[]; dir: TSecretFolder[] }>(
'/api/v1/folders',
{
params: {
workspaceId,
environment,
parentFolderId,
parentFolderPath
}
}
);
return data;
};
export const useGetProjectFolders = ({ export const useGetProjectFolders = ({
workspaceId, workspaceId,
parentFolderId, parentFolderId,
@@ -27,19 +48,7 @@ export const useGetProjectFolders = ({
useQuery({ useQuery({
queryKey: queryKeys.getSecretFolders(workspaceId, environment, parentFolderId), queryKey: queryKeys.getSecretFolders(workspaceId, environment, parentFolderId),
enabled: Boolean(workspaceId) && Boolean(environment) && !isPaused, enabled: Boolean(workspaceId) && Boolean(environment) && !isPaused,
queryFn: async () => { queryFn: async () => fetchProjectFolders(workspaceId, environment, parentFolderId),
const { data } = await apiRequest.get<{ folders: TSecretFolder[]; dir: TSecretFolder[] }>(
'/api/v1/folders',
{
params: {
workspaceId,
environment,
parentFolderId
}
}
);
return data;
},
select: useCallback( select: useCallback(
({ folders, dir }: { folders: TSecretFolder[]; dir: TSecretFolder[] }) => ({ ({ folders, dir }: { folders: TSecretFolder[]; dir: TSecretFolder[] }) => ({
dir, dir,
@@ -53,6 +62,25 @@ export const useGetProjectFolders = ({
) )
}); });
export const useGetProjectFoldersBatch = ({
folders = [],
isPaused,
parentFolderPath
}: GetProjectFoldersBatchDTO) =>
useQueries({
queries: folders.map(({ workspaceId, environment, parentFolderId }) => ({
queryKey: queryKeys.getSecretFolders(workspaceId, environment, parentFolderPath),
queryFn: async () =>
fetchProjectFolders(workspaceId, environment, parentFolderId, parentFolderPath),
enabled: Boolean(workspaceId) && Boolean(environment) && !isPaused,
select: (data: { folders: TSecretFolder[]; dir: TSecretFolder[] }) => ({
environment,
folders: data.folders,
dir: data.dir
})
}))
});
export const useCreateFolder = () => { export const useCreateFolder = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();

View File

@@ -11,6 +11,12 @@ export type GetProjectFoldersDTO = {
sortDir?: 'asc' | 'desc'; sortDir?: 'asc' | 'desc';
}; };
export type GetProjectFoldersBatchDTO = {
folders: Omit<GetProjectFoldersDTO, 'isPaused' | 'sortDir'>[];
isPaused?: boolean;
parentFolderPath?: string;
};
export type CreateFolderDTO = { export type CreateFolderDTO = {
workspaceId: string; workspaceId: string;
environment: string; environment: string;

View File

@@ -1,4 +1,5 @@
/* eslint-disable no-param-reassign */ /* eslint-disable no-param-reassign */
import { useCallback } from 'react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { import {
@@ -29,14 +30,16 @@ export const secretKeys = {
const fetchProjectEncryptedSecrets = async ( const fetchProjectEncryptedSecrets = async (
workspaceId: string, workspaceId: string,
env: string | string[], env: string | string[],
folderId?: string folderId?: string,
secretPath?: string
) => { ) => {
if (typeof env === 'string') { if (typeof env === 'string') {
const { data } = await apiRequest.get<{ secrets: EncryptedSecret[] }>('/api/v2/secrets', { const { data } = await apiRequest.get<{ secrets: EncryptedSecret[] }>('/api/v2/secrets', {
params: { params: {
environment: env, environment: env,
workspaceId, workspaceId,
folderId: folderId || undefined folderId: folderId || undefined,
secretPath
} }
}); });
return data.secrets; return data.secrets;
@@ -52,7 +55,8 @@ const fetchProjectEncryptedSecrets = async (
params: { params: {
environment: envPoint, environment: envPoint,
workspaceId, workspaceId,
folderId folderId,
secretPath
} }
}); });
allEnvData = allEnvData.concat(data.secrets); allEnvData = allEnvData.concat(data.secrets);
@@ -77,7 +81,7 @@ export const useGetProjectSecrets = ({
enabled: Boolean(decryptFileKey && workspaceId && env) && !isPaused, enabled: Boolean(decryptFileKey && workspaceId && env) && !isPaused,
queryKey: secretKeys.getProjectSecret(workspaceId, env, folderId), queryKey: secretKeys.getProjectSecret(workspaceId, env, folderId),
queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env, folderId), queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env, folderId),
select: (data) => { select: useCallback((data: EncryptedSecret[]) => {
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY') as string; const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY') as string;
const latestKey = decryptFileKey; const latestKey = decryptFileKey;
const key = decryptAssymmetric({ const key = decryptAssymmetric({
@@ -146,21 +150,24 @@ export const useGetProjectSecrets = ({
} }
}); });
return { secrets: sharedSecrets }; return { secrets: sharedSecrets };
} }, [decryptFileKey])
}); });
export const useGetProjectSecretsByKey = ({ export const useGetProjectSecretsByKey = ({
workspaceId, workspaceId,
env, env,
decryptFileKey, decryptFileKey,
isPaused isPaused,
folderId,
secretPath
}: GetProjectSecretsDTO) => }: GetProjectSecretsDTO) =>
useQuery({ useQuery({
// wait for all values to be available // wait for all values to be available
enabled: Boolean(decryptFileKey && workspaceId && env) && !isPaused, enabled: Boolean(decryptFileKey && workspaceId && env) && !isPaused,
queryKey: secretKeys.getProjectSecret(workspaceId, env), // right now secretpath is passed as folderid as only this is used in overview
queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env), queryKey: secretKeys.getProjectSecret(workspaceId, env, secretPath),
select: (data) => { queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env, folderId, secretPath),
select: useCallback((data: EncryptedSecret[]) => {
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY') as string; const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY') as string;
const latestKey = decryptFileKey; const latestKey = decryptFileKey;
const key = decryptAssymmetric({ const key = decryptAssymmetric({
@@ -235,7 +242,7 @@ export const useGetProjectSecretsByKey = ({
}); });
return { secrets: sharedSecrets, uniqueSecCount: Object.keys(uniqSecKeys).length }; return { secrets: sharedSecrets, uniqueSecCount: Object.keys(uniqSecKeys).length };
} }, [decryptFileKey])
}); });
const fetchEncryptedSecretVersion = async (secretId: string, offset: number, limit: number) => { const fetchEncryptedSecretVersion = async (secretId: string, offset: number, limit: number) => {
@@ -256,7 +263,7 @@ export const useGetSecretVersion = (dto: GetSecretVersionsDTO) =>
enabled: Boolean(dto.secretId && dto.decryptFileKey), enabled: Boolean(dto.secretId && dto.decryptFileKey),
queryKey: secretKeys.getSecretVersion(dto.secretId), queryKey: secretKeys.getSecretVersion(dto.secretId),
queryFn: () => fetchEncryptedSecretVersion(dto.secretId, dto.offset, dto.limit), queryFn: () => fetchEncryptedSecretVersion(dto.secretId, dto.offset, dto.limit),
select: (data) => { select: useCallback((data: EncryptedSecretVersion[]) => {
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY') as string; const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY') as string;
const latestKey = dto.decryptFileKey; const latestKey = dto.decryptFileKey;
const key = decryptAssymmetric({ const key = decryptAssymmetric({
@@ -278,7 +285,7 @@ export const useGetSecretVersion = (dto: GetSecretVersionsDTO) =>
}) })
})) }))
.sort((a, b) => b.createdAt.localeCompare(a.createdAt)); .sort((a, b) => b.createdAt.localeCompare(a.createdAt));
} }, [])
}); });
export const useBatchSecretsOp = () => { export const useBatchSecretsOp = () => {

View File

@@ -96,6 +96,7 @@ export type GetProjectSecretsDTO = {
env: string | string[]; env: string | string[];
decryptFileKey: UserWsKeyPair; decryptFileKey: UserWsKeyPair;
folderId?: string; folderId?: string;
secretPath?: string;
isPaused?: boolean; isPaused?: boolean;
onSuccess?: (data: DecryptedSecret[]) => void; onSuccess?: (data: DecryptedSecret[]) => void;
}; };

View File

@@ -12,5 +12,5 @@ export type SubscriptionPlan = {
tier: number; tier: number;
workspaceLimit: number; workspaceLimit: number;
workspacesUsed: number; workspacesUsed: number;
envLimit: number; environmentLimit: number;
}; };

View File

@@ -3,6 +3,7 @@ import SecurityClient from '@app/components/utilities/SecurityClient';
interface Props { interface Props {
integrationAuthId: string; integrationAuthId: string;
isActive: boolean; isActive: boolean;
secretPath: string;
app: string | null; app: string | null;
appId: string | null; appId: string | null;
sourceEnvironment: string; sourceEnvironment: string;
@@ -20,19 +21,20 @@ interface Props {
* @param {String} obj.accessToken - id of integration authorization for which to create the integration * @param {String} obj.accessToken - id of integration authorization for which to create the integration
* @returns * @returns
*/ */
const createIntegration = ({ const createIntegration = ({
integrationAuthId, integrationAuthId,
isActive, isActive,
app, app,
appId, appId,
sourceEnvironment, sourceEnvironment,
targetEnvironment, targetEnvironment,
targetEnvironmentId, targetEnvironmentId,
targetService, targetService,
targetServiceId, targetServiceId,
owner, owner,
path, path,
region region,
secretPath
}: Props) => }: Props) =>
SecurityClient.fetchCall('/api/v1/integration', { SecurityClient.fetchCall('/api/v1/integration', {
method: 'POST', method: 'POST',
@@ -40,18 +42,19 @@ const createIntegration = ({
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
body: JSON.stringify({ body: JSON.stringify({
integrationAuthId, integrationAuthId,
isActive, isActive,
app, app,
appId, appId,
sourceEnvironment, sourceEnvironment,
targetEnvironment, targetEnvironment,
targetEnvironmentId, targetEnvironmentId,
targetService, targetService,
targetServiceId, targetServiceId,
owner, owner,
path, path,
region region,
secretPath
}) })
}).then(async (res) => { }).then(async (res) => {
if (res && res.status === 200) { if (res && res.status === 200) {
@@ -61,4 +64,4 @@ const createIntegration = ({
return undefined; return undefined;
}); });
export default createIntegration; export default createIntegration;

View File

@@ -12,13 +12,6 @@ const Dashboard = () => {
const queryEnv = router.query.env as string; const queryEnv = router.query.env as string;
const isOverviewMode = !queryEnv; const isOverviewMode = !queryEnv;
const onExploreEnv = (slug: string) => {
router.push({
pathname: router.pathname,
query: { ...router.query, env: slug }
});
};
return ( return (
<> <>
<Head> <Head>
@@ -29,11 +22,7 @@ const Dashboard = () => {
<meta name="og:description" content={String(t('dashboard.og-description'))} /> <meta name="og:description" content={String(t('dashboard.og-description'))} />
</Head> </Head>
<div className="h-full"> <div className="h-full">
{isOverviewMode ? ( {isOverviewMode ? <DashboardEnvOverview /> : <DashboardPage envFromTop={queryEnv} />}
<DashboardEnvOverview onEnvChange={onExploreEnv} />
) : (
<DashboardPage envFromTop={queryEnv} />
)}
</div> </div>
</> </>
); );

View File

@@ -44,6 +44,7 @@ interface Integration {
integration: string; integration: string;
targetEnvironment: string; targetEnvironment: string;
workspace: string; workspace: string;
secretPath:string;
integrationAuth: string; integrationAuth: string;
} }
@@ -441,7 +442,15 @@ export default function Integrations() {
handleDeleteIntegrationAuth={handleDeleteIntegrationAuth} handleDeleteIntegrationAuth={handleDeleteIntegrationAuth}
/> />
) : ( ) : (
<div /> <>
<div className="m-4 mt-7 flex max-w-5xl flex-col items-start justify-between px-2 text-xl">
<h1 className="text-3xl font-semibold">{t('integrations.cloud-integrations')}</h1>
<p className="text-base text-gray-400">{t('integrations.click-to-start')}</p>
</div>
<div className="mx-6 grid max-w-5xl grid-cols-4 grid-rows-2 gap-4">
{[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16].map(elem => <div key={elem} className="bg-mineshaft-800 border border-mineshaft-600 animate-pulse h-32 rounded-md"/>)}
</div>
</>
)} )}
<FrameworkIntegrationSection frameworks={frameworkIntegrationOptions as any} /> <FrameworkIntegrationSection frameworks={frameworkIntegrationOptions as any} />
</div> </div>

View File

@@ -56,6 +56,7 @@ export default function AWSParameterStoreCreateIntegrationPage() {
const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? ''); const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? '');
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(''); const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
const [secretPath, setSecretPath] = useState('/');
const [selectedAWSRegion, setSelectedAWSRegion] = useState(''); const [selectedAWSRegion, setSelectedAWSRegion] = useState('');
const [path, setPath] = useState(''); const [path, setPath] = useState('');
const [pathErrorText, setPathErrorText] = useState(''); const [pathErrorText, setPathErrorText] = useState('');
@@ -69,9 +70,9 @@ export default function AWSParameterStoreCreateIntegrationPage() {
} }
}, [workspace]); }, [workspace]);
const isValidAWSParameterStorePath = (secretPath: string) => { const isValidAWSParameterStorePath = (awsStorePath: string) => {
const pattern = /^\/([\w-]+\/)*[\w-]+\/$/; const pattern = /^\/([\w-]+\/)*[\w-]+\/$/;
return pattern.test(secretPath) && secretPath.length <= 2048; return pattern.test(awsStorePath) && awsStorePath.length <= 2048;
}; };
const handleButtonClick = async () => { const handleButtonClick = async () => {
@@ -101,7 +102,8 @@ export default function AWSParameterStoreCreateIntegrationPage() {
targetServiceId: null, targetServiceId: null,
owner: null, owner: null,
path, path,
region: selectedAWSRegion region: selectedAWSRegion,
secretPath
}); });
setIsLoading(false); setIsLoading(false);
@@ -133,6 +135,13 @@ export default function AWSParameterStoreCreateIntegrationPage() {
))} ))}
</Select> </Select>
</FormControl> </FormControl>
<FormControl label="Secrets Path">
<Input
value={secretPath}
onChange={(evt) => setSecretPath(evt.target.value)}
placeholder="Provide a path, default is /"
/>
</FormControl>
<FormControl label="AWS Region"> <FormControl label="AWS Region">
<Select <Select
value={selectedAWSRegion} value={selectedAWSRegion}

View File

@@ -56,6 +56,7 @@ export default function AWSSecretManagerCreateIntegrationPage() {
const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? ''); const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? '');
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(''); const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
const [secretPath, setSecretPath] = useState('/');
const [selectedAWSRegion, setSelectedAWSRegion] = useState(''); const [selectedAWSRegion, setSelectedAWSRegion] = useState('');
const [targetSecretName, setTargetSecretName] = useState(''); const [targetSecretName, setTargetSecretName] = useState('');
const [targetSecretNameErrorText, setTargetSecretNameErrorText] = useState(''); const [targetSecretNameErrorText, setTargetSecretNameErrorText] = useState('');
@@ -100,7 +101,8 @@ export default function AWSSecretManagerCreateIntegrationPage() {
targetServiceId: null, targetServiceId: null,
owner: null, owner: null,
path: null, path: null,
region: selectedAWSRegion region: selectedAWSRegion,
secretPath
}); });
setIsLoading(false); setIsLoading(false);
@@ -132,6 +134,13 @@ export default function AWSSecretManagerCreateIntegrationPage() {
))} ))}
</Select> </Select>
</FormControl> </FormControl>
<FormControl label="Secrets Path">
<Input
value={secretPath}
onChange={(evt) => setSecretPath(evt.target.value)}
placeholder="Provide a path, default is /"
/>
</FormControl>
<FormControl label="AWS Region"> <FormControl label="AWS Region">
<Select <Select
value={selectedAWSRegion} value={selectedAWSRegion}

View File

@@ -24,6 +24,7 @@ export default function AzureKeyVaultCreateIntegrationPage() {
const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? ''); const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? '');
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(''); const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
const [secretPath, setSecretPath] = useState('/');
const [vaultBaseUrl, setVaultBaseUrl] = useState(''); const [vaultBaseUrl, setVaultBaseUrl] = useState('');
const [vaultBaseUrlErrorText, setVaultBaseUrlErrorText] = useState(''); const [vaultBaseUrlErrorText, setVaultBaseUrlErrorText] = useState('');
@@ -63,7 +64,8 @@ export default function AzureKeyVaultCreateIntegrationPage() {
targetServiceId: null, targetServiceId: null,
owner: null, owner: null,
path: null, path: null,
region: null region: null,
secretPath
}); });
setIsLoading(false); setIsLoading(false);
@@ -93,6 +95,13 @@ export default function AzureKeyVaultCreateIntegrationPage() {
))} ))}
</Select> </Select>
</FormControl> </FormControl>
<FormControl label="Secrets Path">
<Input
value={secretPath}
onChange={(evt) => setSecretPath(evt.target.value)}
placeholder="Provide a path, default is /"
/>
</FormControl>
<FormControl <FormControl
label="Vault URI" label="Vault URI"
errorText={vaultBaseUrlErrorText} errorText={vaultBaseUrlErrorText}

View File

@@ -2,7 +2,15 @@ import { useEffect, useState } from 'react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import queryString from 'query-string'; import queryString from 'query-string';
import { Button, Card, CardTitle, FormControl, Select, SelectItem } from '../../../components/v2'; import {
Button,
Card,
CardTitle,
FormControl,
Input,
Select,
SelectItem
} from '../../../components/v2';
import { import {
useGetIntegrationAuthApps, useGetIntegrationAuthApps,
useGetIntegrationAuthById useGetIntegrationAuthById
@@ -22,6 +30,8 @@ export default function ChecklyCreateIntegrationPage() {
}); });
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(''); const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
const [secretPath, setSecretPath] = useState('/');
const [targetApp, setTargetApp] = useState(''); const [targetApp, setTargetApp] = useState('');
const [targetAppId, setTargetAppId] = useState(''); const [targetAppId, setTargetAppId] = useState('');
@@ -63,7 +73,8 @@ export default function ChecklyCreateIntegrationPage() {
targetServiceId: null, targetServiceId: null,
owner: null, owner: null,
path: null, path: null,
region: null region: null,
secretPath
}); });
setIsLoading(false); setIsLoading(false);
@@ -80,8 +91,13 @@ export default function ChecklyCreateIntegrationPage() {
integrationAuthApps && integrationAuthApps &&
targetApp ? ( targetApp ? (
<div className="flex h-full w-full items-center justify-center bg-gradient-to-tr from-mineshaft-900 to-bunker-900"> <div className="flex h-full w-full items-center justify-center bg-gradient-to-tr from-mineshaft-900 to-bunker-900">
<Card className="max-w-lg rounded-md p-0 border border-mineshaft-600"> <Card className="max-w-lg rounded-md border border-mineshaft-600 p-0">
<CardTitle className="text-left px-6" subTitle="Choose which environment in Infisical you want to sync with your Checkly account.">Checkly Integration</CardTitle> <CardTitle
className="px-6 text-left"
subTitle="Choose which environment in Infisical you want to sync with your Checkly account."
>
Checkly Integration
</CardTitle>
<FormControl label="Infisical Project Environment" className="mt-2 px-6"> <FormControl label="Infisical Project Environment" className="mt-2 px-6">
<Select <Select
value={selectedSourceEnvironment} value={selectedSourceEnvironment}
@@ -98,6 +114,13 @@ export default function ChecklyCreateIntegrationPage() {
))} ))}
</Select> </Select>
</FormControl> </FormControl>
<FormControl label="Secrets Path">
<Input
value={secretPath}
onChange={(evt) => setSecretPath(evt.target.value)}
placeholder="Provide a path, default is /"
/>
</FormControl>
<FormControl label="Checkly Account" className="mt-4 px-6"> <FormControl label="Checkly Account" className="mt-4 px-6">
<Select <Select
value={targetApp} value={targetApp}

View File

@@ -2,7 +2,15 @@ import { useEffect, useState } from 'react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import queryString from 'query-string'; import queryString from 'query-string';
import { Button, Card, CardTitle, FormControl, Select, SelectItem } from '../../../components/v2'; import {
Button,
Card,
CardTitle,
FormControl,
Input,
Select,
SelectItem
} from '../../../components/v2';
import { import {
useGetIntegrationAuthApps, useGetIntegrationAuthApps,
useGetIntegrationAuthById useGetIntegrationAuthById
@@ -22,6 +30,8 @@ export default function CircleCICreateIntegrationPage() {
}); });
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(''); const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
const [secretPath, setSecretPath] = useState('/');
const [targetApp, setTargetApp] = useState(''); const [targetApp, setTargetApp] = useState('');
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
@@ -62,7 +72,8 @@ export default function CircleCICreateIntegrationPage() {
targetServiceId: null, targetServiceId: null,
owner: null, owner: null,
path: null, path: null,
region: null region: null,
secretPath
}); });
setIsLoading(false); setIsLoading(false);
@@ -97,6 +108,13 @@ export default function CircleCICreateIntegrationPage() {
))} ))}
</Select> </Select>
</FormControl> </FormControl>
<FormControl label="Secrets Path">
<Input
value={secretPath}
onChange={(evt) => setSecretPath(evt.target.value)}
placeholder="Provide a path, default is /"
/>
</FormControl>
<FormControl label="CircleCI Project" className="mt-4"> <FormControl label="CircleCI Project" className="mt-4">
<Select <Select
value={targetApp} value={targetApp}

View File

@@ -2,7 +2,15 @@ import { useEffect, useState } from 'react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import queryString from 'query-string'; import queryString from 'query-string';
import { Button, Card, CardTitle, FormControl, Select, SelectItem } from '../../../components/v2'; import {
Button,
Card,
CardTitle,
FormControl,
Input,
Select,
SelectItem
} from '../../../components/v2';
import { import {
useGetIntegrationAuthApps, useGetIntegrationAuthApps,
useGetIntegrationAuthById useGetIntegrationAuthById
@@ -22,6 +30,8 @@ export default function FlyioCreateIntegrationPage() {
}); });
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(''); const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
const [secretPath, setSecretPath] = useState('/');
const [targetApp, setTargetApp] = useState(''); const [targetApp, setTargetApp] = useState('');
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
@@ -61,7 +71,8 @@ export default function FlyioCreateIntegrationPage() {
targetServiceId: null, targetServiceId: null,
owner: null, owner: null,
path: null, path: null,
region: null region: null,
secretPath
}); });
setIsLoading(false); setIsLoading(false);
@@ -96,6 +107,13 @@ export default function FlyioCreateIntegrationPage() {
))} ))}
</Select> </Select>
</FormControl> </FormControl>
<FormControl label="Secrets Path">
<Input
value={secretPath}
onChange={(evt) => setSecretPath(evt.target.value)}
placeholder="Provide a path, default is /"
/>
</FormControl>
<FormControl label="Fly.io App" className="mt-4"> <FormControl label="Fly.io App" className="mt-4">
<Select <Select
value={targetApp} value={targetApp}

View File

@@ -2,7 +2,15 @@ import { useEffect, useState } from 'react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import queryString from 'query-string'; import queryString from 'query-string';
import { Button, Card, CardTitle, FormControl, Select, SelectItem } from '../../../components/v2'; import {
Button,
Card,
CardTitle,
FormControl,
Input,
Select,
SelectItem
} from '../../../components/v2';
import { import {
useGetIntegrationAuthApps, useGetIntegrationAuthApps,
useGetIntegrationAuthById useGetIntegrationAuthById
@@ -22,6 +30,7 @@ export default function GitHubCreateIntegrationPage() {
}); });
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(''); const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
const [secretPath, setSecretPath] = useState('/');
const [targetAppId, setTargetAppId] = useState(''); const [targetAppId, setTargetAppId] = useState('');
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
@@ -66,7 +75,8 @@ export default function GitHubCreateIntegrationPage() {
targetServiceId: null, targetServiceId: null,
owner: targetApp.owner, owner: targetApp.owner,
path: null, path: null,
region: null region: null,
secretPath
}); });
setIsLoading(false); setIsLoading(false);
@@ -100,6 +110,13 @@ export default function GitHubCreateIntegrationPage() {
))} ))}
</Select> </Select>
</FormControl> </FormControl>
<FormControl label="Secrets Path">
<Input
value={secretPath}
onChange={(evt) => setSecretPath(evt.target.value)}
placeholder="Provide a path, default is /"
/>
</FormControl>
<FormControl label="GitHub Repo" className="mt-4"> <FormControl label="GitHub Repo" className="mt-4">
<Select <Select
value={targetAppId} value={targetAppId}

View File

@@ -2,13 +2,13 @@ import { useEffect, useState } from 'react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import queryString from 'query-string'; import queryString from 'query-string';
import { import {
Button, Button,
Card, Card,
CardTitle, CardTitle,
FormControl, FormControl,
Input, Input,
Select, Select,
SelectItem SelectItem
} from '../../../components/v2'; } from '../../../components/v2';
import { import {
@@ -44,6 +44,7 @@ export default function GitLabCreateIntegrationPage() {
const [targetEntity, setTargetEntity] = useState(gitLabEntities[0].value); const [targetEntity, setTargetEntity] = useState(gitLabEntities[0].value);
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(''); const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
const [secretPath, setSecretPath] = useState('/');
const [targetAppId, setTargetAppId] = useState(''); const [targetAppId, setTargetAppId] = useState('');
const [targetEnvironment, setTargetEnvironment] = useState(''); const [targetEnvironment, setTargetEnvironment] = useState('');
@@ -101,7 +102,8 @@ export default function GitLabCreateIntegrationPage() {
targetServiceId: null, targetServiceId: null,
owner: null, owner: null,
path: null, path: null,
region: null region: null,
secretPath
}); });
setIsLoading(false); setIsLoading(false);
@@ -136,6 +138,13 @@ export default function GitLabCreateIntegrationPage() {
))} ))}
</Select> </Select>
</FormControl> </FormControl>
<FormControl label="Secrets Path">
<Input
value={secretPath}
onChange={(evt) => setSecretPath(evt.target.value)}
placeholder="Provide a path, default is /"
/>
</FormControl>
<FormControl label="GitLab Integration Type"> <FormControl label="GitLab Integration Type">
<Select <Select
value={targetEntity} value={targetEntity}
@@ -198,13 +207,11 @@ export default function GitLabCreateIntegrationPage() {
)} )}
</Select> </Select>
</FormControl> </FormControl>
<FormControl <FormControl label="GitLab Environment Scope (Optional)">
label="GitLab Environment Scope (Optional)" <Input
> placeholder="*"
<Input value={targetEnvironment}
placeholder="*" onChange={(e) => setTargetEnvironment(e.target.value)}
value={targetEnvironment}
onChange={(e) => setTargetEnvironment(e.target.value)}
/> />
</FormControl> </FormControl>
<Button <Button

View File

@@ -1,8 +1,16 @@
import { useState } from 'react'; import { useState } from 'react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import queryString from 'query-string'; import queryString from 'query-string';
import { Button, Card, CardTitle, FormControl, Input, Select, SelectItem } from '../../../components/v2';
import {
Button,
Card,
CardTitle,
FormControl,
Input,
Select,
SelectItem
} from '../../../components/v2';
import { useGetIntegrationAuthById } from '../../../hooks/api/integrationAuth'; import { useGetIntegrationAuthById } from '../../../hooks/api/integrationAuth';
import { useGetWorkspaceById } from '../../../hooks/api/workspace'; import { useGetWorkspaceById } from '../../../hooks/api/workspace';
import createIntegration from '../../api/integrations/createIntegration'; import createIntegration from '../../api/integrations/createIntegration';
@@ -16,60 +24,58 @@ export default function HashiCorpVaultCreateIntegrationPage() {
const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? ''); const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? '');
const [vaultEnginePath, setVaultEnginePath] = useState(''); const [vaultEnginePath, setVaultEnginePath] = useState('');
const [vaultEnginePathErrorText, setVaultEnginePathErrorText ] = useState(''); const [vaultEnginePathErrorText, setVaultEnginePathErrorText] = useState('');
const [vaultSecretPath, setVaultSecretPath] = useState(''); const [vaultSecretPath, setVaultSecretPath] = useState('');
const [vaultSecretPathErrorText, setVaultSecretPathErrorText] = useState(''); const [vaultSecretPathErrorText, setVaultSecretPathErrorText] = useState('');
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(''); const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
const [secretPath, setSecretPath] = useState('/');
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const isValidVaultPath = (secretPath: string) => { const isValidVaultPath = (vaultPath: string) => {
return !( return !(vaultPath.length === 0 || vaultPath.startsWith('/') || vaultPath.endsWith('/'));
secretPath.length === 0 ||
secretPath.startsWith('/') ||
secretPath.endsWith('/')
);
}; };
const handleButtonClick = async () => { const handleButtonClick = async () => {
try { try {
if (!integrationAuth?._id) return; if (!integrationAuth?._id) return;
if (!isValidVaultPath(vaultEnginePath)) { if (!isValidVaultPath(vaultEnginePath)) {
setVaultEnginePathErrorText('Vault KV Secrets Engine Path must be valid like kv'); setVaultEnginePathErrorText('Vault KV Secrets Engine Path must be valid like kv');
} else { } else {
setVaultEnginePathErrorText(''); setVaultEnginePathErrorText('');
} }
if (!isValidVaultPath(vaultSecretPath)) {
setVaultSecretPathErrorText('Vault Secret(s) Path must be valid like machine/dev');
} else {
setVaultSecretPathErrorText('');
}
if (!isValidVaultPath || !isValidVaultPath(vaultSecretPath)) return;
setIsLoading(true); if (!isValidVaultPath(vaultSecretPath)) {
setVaultSecretPathErrorText('Vault Secret(s) Path must be valid like machine/dev');
await createIntegration({ } else {
integrationAuthId: integrationAuth?._id, setVaultSecretPathErrorText('');
isActive: true, }
app: vaultEnginePath,
appId: null,
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment: null,
targetEnvironmentId: null,
targetService: null,
targetServiceId: null,
owner: null,
path: vaultSecretPath,
region: null
});
setIsLoading(false);
router.push(`/integrations/${localStorage.getItem('projectData.id')}`); if (!isValidVaultPath || !isValidVaultPath(vaultSecretPath)) return;
setIsLoading(true);
await createIntegration({
integrationAuthId: integrationAuth?._id,
isActive: true,
app: vaultEnginePath,
appId: null,
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment: null,
targetEnvironmentId: null,
targetService: null,
targetServiceId: null,
owner: null,
path: vaultSecretPath,
region: null,
secretPath
});
setIsLoading(false);
router.push(`/integrations/${localStorage.getItem('projectData.id')}`);
} catch (err) { } catch (err) {
console.error(err); console.error(err);
} }
@@ -95,34 +101,41 @@ export default function HashiCorpVaultCreateIntegrationPage() {
))} ))}
</Select> </Select>
</FormControl> </FormControl>
<FormControl label="Secrets Path">
<Input
value={secretPath}
onChange={(evt) => setSecretPath(evt.target.value)}
placeholder="Provide a path, default is /"
/>
</FormControl>
<FormControl <FormControl
label="Vault KV Secrets Engine Path" label="Vault KV Secrets Engine Path"
errorText={vaultEnginePathErrorText} errorText={vaultEnginePathErrorText}
isError={vaultEnginePathErrorText !== '' ?? false} isError={vaultEnginePathErrorText !== '' ?? false}
> >
<Input <Input
placeholder="kv" placeholder="kv"
value={vaultEnginePath} value={vaultEnginePath}
onChange={(e) => setVaultEnginePath(e.target.value)} onChange={(e) => setVaultEnginePath(e.target.value)}
/> />
</FormControl> </FormControl>
<FormControl <FormControl
label="Vault Secret(s) Path" label="Vault Secret(s) Path"
errorText={vaultSecretPathErrorText} errorText={vaultSecretPathErrorText}
isError={vaultSecretPathErrorText !== '' ?? false} isError={vaultSecretPathErrorText !== '' ?? false}
> >
<Input <Input
placeholder="machine/dev" placeholder="machine/dev"
value={vaultSecretPath} value={vaultSecretPath}
onChange={(e) => setVaultSecretPath(e.target.value)} onChange={(e) => setVaultSecretPath(e.target.value)}
/> />
</FormControl> </FormControl>
<Button <Button
onClick={handleButtonClick} onClick={handleButtonClick}
color="mineshaft" color="mineshaft"
className="mt-4" className="mt-4"
isLoading={isLoading} isLoading={isLoading}
isDisabled={!(isValidVaultPath(vaultEnginePath) && isValidVaultPath(vaultSecretPath))} isDisabled={!(isValidVaultPath(vaultEnginePath) && isValidVaultPath(vaultSecretPath))}
> >
Create Integration Create Integration
</Button> </Button>

View File

@@ -2,7 +2,15 @@ import { useEffect, useState } from 'react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import queryString from 'query-string'; import queryString from 'query-string';
import { Button, Card, CardTitle, FormControl, Select, SelectItem } from '../../../components/v2'; import {
Button,
Card,
CardTitle,
FormControl,
Input,
Select,
SelectItem
} from '../../../components/v2';
import { import {
useGetIntegrationAuthApps, useGetIntegrationAuthApps,
useGetIntegrationAuthById useGetIntegrationAuthById
@@ -23,6 +31,7 @@ export default function HerokuCreateIntegrationPage() {
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(''); const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
const [targetApp, setTargetApp] = useState(''); const [targetApp, setTargetApp] = useState('');
const [secretPath, setSecretPath] = useState('/');
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
@@ -60,7 +69,8 @@ export default function HerokuCreateIntegrationPage() {
targetServiceId: null, targetServiceId: null,
owner: null, owner: null,
path: null, path: null,
region: null region: null,
secretPath
}); });
setIsLoading(false); setIsLoading(false);
@@ -94,6 +104,13 @@ export default function HerokuCreateIntegrationPage() {
))} ))}
</Select> </Select>
</FormControl> </FormControl>
<FormControl label="Secrets Path">
<Input
value={secretPath}
onChange={(evt) => setSecretPath(evt.target.value)}
placeholder="Provide a path, default is /"
/>
</FormControl>
<FormControl label="Heroku App" className="mt-4"> <FormControl label="Heroku App" className="mt-4">
<Select <Select
value={targetApp} value={targetApp}

View File

@@ -2,7 +2,15 @@ import { useEffect, useState } from 'react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import queryString from 'query-string'; import queryString from 'query-string';
import { Button, Card, CardTitle, FormControl, Select, SelectItem } from '../../../components/v2'; import {
Button,
Card,
CardTitle,
FormControl,
Input,
Select,
SelectItem
} from '../../../components/v2';
import { import {
useGetIntegrationAuthApps, useGetIntegrationAuthApps,
useGetIntegrationAuthById useGetIntegrationAuthById
@@ -31,7 +39,7 @@ export default function NetlifyCreateIntegrationPage() {
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(''); const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
const [targetApp, setTargetApp] = useState(''); const [targetApp, setTargetApp] = useState('');
const [targetEnvironment, setTargetEnvironment] = useState(''); const [targetEnvironment, setTargetEnvironment] = useState('');
const [secretPath, setSecretPath] = useState('/');
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
useEffect(() => { useEffect(() => {
@@ -71,7 +79,8 @@ export default function NetlifyCreateIntegrationPage() {
targetServiceId: null, targetServiceId: null,
owner: null, owner: null,
path: null, path: null,
region: null region: null,
secretPath
}); });
setIsLoading(false); setIsLoading(false);
@@ -106,6 +115,13 @@ export default function NetlifyCreateIntegrationPage() {
))} ))}
</Select> </Select>
</FormControl> </FormControl>
<FormControl label="Secrets Path">
<Input
value={secretPath}
onChange={(evt) => setSecretPath(evt.target.value)}
placeholder="Provide a path, default is /"
/>
</FormControl>
<FormControl label="Netlify Site"> <FormControl label="Netlify Site">
<Select <Select
value={targetApp} value={targetApp}

View File

@@ -2,7 +2,15 @@ import { useEffect, useState } from 'react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import queryString from 'query-string'; import queryString from 'query-string';
import { Button, Card, CardTitle, FormControl, Select, SelectItem } from '../../../components/v2'; import {
Button,
Card,
CardTitle,
FormControl,
Input,
Select,
SelectItem
} from '../../../components/v2';
import { import {
useGetIntegrationAuthApps, useGetIntegrationAuthApps,
useGetIntegrationAuthById, useGetIntegrationAuthById,
@@ -20,6 +28,7 @@ export default function RailwayCreateIntegrationPage() {
const [targetServiceId, setTargetServiceId] = useState(''); const [targetServiceId, setTargetServiceId] = useState('');
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(''); const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
const [secretPath, setSecretPath] = useState('/');
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const { integrationAuthId } = queryString.parse(router.asPath.split('?')[1]); const { integrationAuthId } = queryString.parse(router.asPath.split('?')[1]);
@@ -99,7 +108,8 @@ export default function RailwayCreateIntegrationPage() {
targetServiceId: targetService ? targetService.serviceId : null, targetServiceId: targetService ? targetService.serviceId : null,
owner: null, owner: null,
path: null, path: null,
region: null region: null,
secretPath
}); });
setIsLoading(false); setIsLoading(false);
@@ -134,6 +144,13 @@ export default function RailwayCreateIntegrationPage() {
))} ))}
</Select> </Select>
</FormControl> </FormControl>
<FormControl label="Secrets Path">
<Input
value={secretPath}
onChange={(evt) => setSecretPath(evt.target.value)}
placeholder="Provide a path, default is /"
/>
</FormControl>
<FormControl label="Railway Project"> <FormControl label="Railway Project">
<Select <Select
value={targetAppId} value={targetAppId}

View File

@@ -2,7 +2,15 @@ import { useEffect, useState } from 'react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import queryString from 'query-string'; import queryString from 'query-string';
import { Button, Card, CardTitle, FormControl, Select, SelectItem } from '../../../components/v2'; import {
Button,
Card,
CardTitle,
FormControl,
Input,
Select,
SelectItem
} from '../../../components/v2';
import { import {
useGetIntegrationAuthApps, useGetIntegrationAuthApps,
useGetIntegrationAuthById useGetIntegrationAuthById
@@ -23,7 +31,7 @@ export default function RenderCreateIntegrationPage() {
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(''); const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
const [targetApp, setTargetApp] = useState(''); const [targetApp, setTargetApp] = useState('');
const [secretPath, setSecretPath] = useState('/');
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
useEffect(() => { useEffect(() => {
@@ -62,7 +70,8 @@ export default function RenderCreateIntegrationPage() {
targetServiceId: null, targetServiceId: null,
owner: null, owner: null,
path: null, path: null,
region: null region: null,
secretPath
}); });
setIsLoading(false); setIsLoading(false);
@@ -97,6 +106,13 @@ export default function RenderCreateIntegrationPage() {
))} ))}
</Select> </Select>
</FormControl> </FormControl>
<FormControl label="Secrets Path">
<Input
value={secretPath}
onChange={(evt) => setSecretPath(evt.target.value)}
placeholder="Provide a path, default is /"
/>
</FormControl>
<FormControl label="Render Service" className="mt-4"> <FormControl label="Render Service" className="mt-4">
<Select <Select
value={targetApp} value={targetApp}

View File

@@ -2,7 +2,15 @@ import { useEffect, useState } from 'react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import queryString from 'query-string'; import queryString from 'query-string';
import { Button, Card, CardTitle, FormControl, Select, SelectItem } from '../../../components/v2'; import {
Button,
Card,
CardTitle,
FormControl,
Input,
Select,
SelectItem
} from '../../../components/v2';
import { import {
useGetIntegrationAuthApps, useGetIntegrationAuthApps,
useGetIntegrationAuthById useGetIntegrationAuthById
@@ -22,6 +30,7 @@ export default function SupabaseCreateIntegrationPage() {
}); });
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(''); const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
const [secretPath, setSecretPath] = useState('/');
const [targetApp, setTargetApp] = useState(''); const [targetApp, setTargetApp] = useState('');
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
@@ -62,7 +71,8 @@ export default function SupabaseCreateIntegrationPage() {
targetServiceId: null, targetServiceId: null,
owner: null, owner: null,
path: null, path: null,
region: null region: null,
secretPath
}); });
setIsLoading(false); setIsLoading(false);
@@ -97,6 +107,13 @@ export default function SupabaseCreateIntegrationPage() {
))} ))}
</Select> </Select>
</FormControl> </FormControl>
<FormControl label="Secrets Path">
<Input
value={secretPath}
onChange={(evt) => setSecretPath(evt.target.value)}
placeholder="Provide a path, default is /"
/>
</FormControl>
<FormControl label="Supabase Project" className="mt-4"> <FormControl label="Supabase Project" className="mt-4">
<Select <Select
value={targetApp} value={targetApp}

View File

@@ -2,7 +2,15 @@ import { useEffect, useState } from 'react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import queryString from 'query-string'; import queryString from 'query-string';
import { Button, Card, CardTitle, FormControl, Select, SelectItem } from '../../../components/v2'; import {
Button,
Card,
CardTitle,
FormControl,
Input,
Select,
SelectItem
} from '../../../components/v2';
import { import {
useGetIntegrationAuthApps, useGetIntegrationAuthApps,
useGetIntegrationAuthById useGetIntegrationAuthById
@@ -23,7 +31,7 @@ export default function TravisCICreateIntegrationPage() {
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(''); const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
const [targetApp, setTargetApp] = useState(''); const [targetApp, setTargetApp] = useState('');
const [secretPath, setSecretPath] = useState('/');
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
useEffect(() => { useEffect(() => {
@@ -62,7 +70,8 @@ export default function TravisCICreateIntegrationPage() {
targetServiceId: null, targetServiceId: null,
owner: null, owner: null,
path: null, path: null,
region: null region: null,
secretPath
}); });
setIsLoading(false); setIsLoading(false);
@@ -97,6 +106,13 @@ export default function TravisCICreateIntegrationPage() {
))} ))}
</Select> </Select>
</FormControl> </FormControl>
<FormControl label="Secrets Path">
<Input
value={secretPath}
onChange={(evt) => setSecretPath(evt.target.value)}
placeholder="Provide a path, default is /"
/>
</FormControl>
<FormControl label="Travis CI Project" className="mt-4"> <FormControl label="Travis CI Project" className="mt-4">
<Select <Select
value={targetApp} value={targetApp}

View File

@@ -2,7 +2,15 @@ import { useEffect, useState } from 'react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import queryString from 'query-string'; import queryString from 'query-string';
import { Button, Card, CardTitle, FormControl, Select, SelectItem } from '../../../components/v2'; import {
Button,
Card,
CardTitle,
FormControl,
Input,
Select,
SelectItem
} from '../../../components/v2';
import { import {
useGetIntegrationAuthApps, useGetIntegrationAuthApps,
useGetIntegrationAuthById, useGetIntegrationAuthById,
@@ -21,6 +29,7 @@ export default function VercelCreateIntegrationPage() {
const router = useRouter(); const router = useRouter();
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(''); const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
const [secretPath, setSecretPath] = useState('/');
const [targetAppId, setTargetAppId] = useState(''); const [targetAppId, setTargetAppId] = useState('');
const [targetEnvironment, setTargetEnvironment] = useState(''); const [targetEnvironment, setTargetEnvironment] = useState('');
const [targetBranch, setTargetBranch] = useState(''); const [targetBranch, setTargetBranch] = useState('');
@@ -84,7 +93,8 @@ export default function VercelCreateIntegrationPage() {
targetServiceId: null, targetServiceId: null,
owner: null, owner: null,
path, path,
region: null region: null,
secretPath
}); });
setIsLoading(false); setIsLoading(false);
@@ -119,6 +129,13 @@ export default function VercelCreateIntegrationPage() {
))} ))}
</Select> </Select>
</FormControl> </FormControl>
<FormControl label="Secrets Path">
<Input
value={secretPath}
onChange={(evt) => setSecretPath(evt.target.value)}
placeholder="Provide a path, default is /"
/>
</FormControl>
<FormControl label="Vercel App"> <FormControl label="Vercel App">
<Select <Select
value={targetAppId} value={targetAppId}

View File

@@ -166,9 +166,9 @@ export default function PasswordReset() {
<p className="mx-auto mb-4 flex w-max justify-center text-2xl font-semibold text-bunker-100 md:text-3xl"> <p className="mx-auto mb-4 flex w-max justify-center text-2xl font-semibold text-bunker-100 md:text-3xl">
Enter your backup key Enter your backup key
</p> </p>
<div className="mt-4 flex flex-row items-center justify-center md:mx-2 md:pb-4"> <div className="flex flex-row items-center justify-center md:pb-4 mt-4 md:mx-2">
<p className="flex w-max max-w-md justify-center text-sm text-gray-400"> <p className="text-sm flex justify-center text-gray-400 w-max max-w-md">
You can find it in your emrgency kit. You had to download the enrgency kit during signup. You can find it in your emergency kit. You had to download the emergency kit during signup.
</p> </p>
</div> </div>
<div className="mt-4 flex max-h-24 w-full items-center justify-center rounded-lg md:mt-0 md:max-h-28 md:p-2"> <div className="mt-4 flex max-h-24 w-full items-center justify-center rounded-lg md:mt-0 md:max-h-28 md:p-2">

View File

@@ -4,20 +4,20 @@
@layer utilities { @layer utilities {
.flex-0 { .flex-0 {
flex:0; flex: 0;
} }
.flex-2 { .flex-2 {
flex-grow: 2; flex-grow: 2;
} }
.flex-3 { .flex-3 {
flex-grow: 3; flex-grow: 3;
} }
} }
@layer components { @layer components {
.secret-table { .secret-table {
@apply bg-mineshaft-800 text-left text-bunker-300 w-full; @apply w-full bg-mineshaft-800 text-left text-bunker-300;
} }
/* padding except for comment column */ /* padding except for comment column */
@@ -29,13 +29,45 @@
@apply py-1 px-1 pr-2 text-sm; @apply py-1 px-1 pr-2 text-sm;
} }
.secret-table th:not(:last-child),.secret-table td:not(:last-child) { .secret-table th:not(:last-child),
.secret-table td:not(:last-child) {
@apply border-r border-mineshaft-600; @apply border-r border-mineshaft-600;
} }
.secret-table tr { .secret-table tr {
@apply border-b border-mineshaft-600; @apply border-b border-mineshaft-600;
} }
.breadcrumb::after,
.breadcrumb::before {
content: '';
height: 60%;
width: 100%;
z-index: -1;
display: block;
position: absolute;
@apply bg-mineshaft-800;
}
.breadcrumb:hover::before {
@apply bg-mineshaft-600;
}
.breadcrumb:hover::after {
@apply bg-mineshaft-600;
}
.breadcrumb::after {
left: 5px;
bottom: -3px;
transform: skew(-30deg);
}
.breadcrumb::before {
left: 5px;
top: -3px;
transform: skew(30deg);
}
} }
@import '@fontsource/inter/400.css'; @import '@fontsource/inter/400.css';

View File

@@ -1,35 +1,31 @@
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { faKey, faMagnifyingGlass } from '@fortawesome/free-solid-svg-icons'; import { faFolderOpen, faKey, faMagnifyingGlass } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { yupResolver } from '@hookform/resolvers/yup';
import NavHeader from '@app/components/navigation/NavHeader'; import NavHeader from '@app/components/navigation/NavHeader';
import { Button, Input, TableContainer, Tooltip } from '@app/components/v2'; import { Button, Input, TableContainer, Tooltip } from '@app/components/v2';
import { useWorkspace } from '@app/context'; import { useWorkspace } from '@app/context';
import { import {
useGetProjectFoldersBatch,
useGetProjectSecretsByKey, useGetProjectSecretsByKey,
useGetUserWsEnvironments, useGetUserWsEnvironments,
useGetUserWsKey useGetUserWsKey
} from '@app/hooks/api'; } from '@app/hooks/api';
import { WorkspaceEnv } from '@app/hooks/api/types';
import { EnvComparisonRow } from './components/EnvComparisonRow'; import { EnvComparisonRow } from './components/EnvComparisonRow';
import { FormData, schema } from './DashboardPage.utils'; import { FolderComparisonRow } from './components/EnvComparisonRow/FolderComparisonRow';
export const DashboardEnvOverview = ({ onEnvChange }: { onEnvChange: any }) => { export const DashboardEnvOverview = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const router = useRouter(); const router = useRouter();
const [selectedEnv, setSelectedEnv] = useState<WorkspaceEnv | null>(null);
const { currentWorkspace, isLoading } = useWorkspace(); const { currentWorkspace, isLoading } = useWorkspace();
const workspaceId = currentWorkspace?._id as string; const workspaceId = currentWorkspace?._id as string;
const { data: latestFileKey } = useGetUserWsKey(workspaceId); const { data: latestFileKey } = useGetUserWsKey(workspaceId);
const [searchFilter, setSearchFilter] = useState(''); const [searchFilter, setSearchFilter] = useState('');
const secretPath = router.query?.secretPath as string;
useEffect(() => { useEffect(() => {
if (!isLoading && !workspaceId && router.isReady) { if (!isLoading && !workspaceId && router.isReady) {
@@ -38,14 +34,7 @@ export const DashboardEnvOverview = ({ onEnvChange }: { onEnvChange: any }) => {
}, [isLoading, workspaceId, router.isReady]); }, [isLoading, workspaceId, router.isReady]);
const { data: wsEnv, isLoading: isEnvListLoading } = useGetUserWsEnvironments({ const { data: wsEnv, isLoading: isEnvListLoading } = useGetUserWsEnvironments({
workspaceId, workspaceId
onSuccess: (data) => {
// get an env with one of the access available
const env = data.find(({ isReadDenied }) => !isReadDenied);
if (env) {
setSelectedEnv(env);
}
}
}); });
const userAvailableEnvs = wsEnv?.filter(({ isReadDenied }) => !isReadDenied); const userAvailableEnvs = wsEnv?.filter(({ isReadDenied }) => !isReadDenied);
@@ -54,17 +43,32 @@ export const DashboardEnvOverview = ({ onEnvChange }: { onEnvChange: any }) => {
workspaceId, workspaceId,
env: userAvailableEnvs?.map((env) => env.slug) ?? [], env: userAvailableEnvs?.map((env) => env.slug) ?? [],
decryptFileKey: latestFileKey!, decryptFileKey: latestFileKey!,
isPaused: false isPaused: false,
secretPath
}); });
const method = useForm<FormData>({ const folders = useGetProjectFoldersBatch({
// why any: well yup inferred ts expects other keys to defined as undefined folders:
defaultValues: secrets as any, userAvailableEnvs?.map((env) => ({
values: secrets as any, environment: env.slug,
mode: 'onBlur', workspaceId
resolver: yupResolver(schema) })) ?? [],
parentFolderPath: secretPath
}); });
const foldersGroupedByEnv = useMemo(() => {
const res: Record<string, Record<string, boolean>> = {};
folders.forEach(({ data }) => {
data?.folders
?.filter(({ name }) => name.toLowerCase().includes(searchFilter))
?.forEach((folder) => {
if (!res?.[folder.name]) res[folder.name] = {};
res[folder.name][data.environment] = true;
});
});
return res;
}, [folders, userAvailableEnvs, searchFilter]);
const numSecretsMissingPerEnv = useMemo(() => { const numSecretsMissingPerEnv = useMemo(() => {
// first get all sec in the env then subtract with total to get missing ones // first get all sec in the env then subtract with total to get missing ones
const secPerEnvMissing: Record<string, number> = Object.fromEntries( const secPerEnvMissing: Record<string, number> = Object.fromEntries(
@@ -81,7 +85,43 @@ export const DashboardEnvOverview = ({ onEnvChange }: { onEnvChange: any }) => {
return secPerEnvMissing; return secPerEnvMissing;
}, [secrets, userAvailableEnvs]); }, [secrets, userAvailableEnvs]);
const isReadOnly = selectedEnv?.isWriteDenied; const onExploreEnv = (slug: string) => {
const query: Record<string, string> = { ...router.query, env: slug };
delete query.secretPath;
// the dir return will have the present directory folder id
// use that when clicking on explore to redirect user to there
const envFolder = folders.find(({ data }) => slug === data?.environment);
const dir = envFolder?.data?.dir?.pop();
if (dir) {
query.folderId = dir.id;
}
router.push({
pathname: router.pathname,
query
});
};
const onFolderClick = (path: string) => {
router.push({
pathname: router.pathname,
query: {
...router.query,
secretPath: `${router.query?.secretPath || ''}/${path}`
}
});
};
const onFolderCrumbClick = (index: number) => {
const newSecPath = secretPath.split('/').filter(Boolean).slice(0, index).join('/');
const query = { ...router.query, secretPath: `/${newSecPath}` } as Record<string, string>;
// root condition
if (index === 0) delete query.secretPath;
router.push({
pathname: router.pathname,
query
});
};
if (isSecretsLoading || isEnvListLoading) { if (isSecretsLoading || isEnvListLoading) {
return ( return (
@@ -91,165 +131,195 @@ export const DashboardEnvOverview = ({ onEnvChange }: { onEnvChange: any }) => {
); );
} }
const filteredSecrets = Object.keys(secrets?.secrets || {})?.filter((secret: any) =>
secret.toUpperCase().includes(searchFilter.toUpperCase())
);
// when secrets is not loading and secrets list is empty // when secrets is not loading and secrets list is empty
const isDashboardSecretEmpty = !isSecretsLoading && !Object.keys(secrets?.secrets || {})?.filter((secret: any) => secret.toUpperCase().includes(searchFilter.toUpperCase()))?.length; const isDashboardSecretEmpty = !isSecretsLoading && !filteredSecrets?.length;
const isFoldersEmtpy =
!folders.some(({ isLoading: isFolderLoading }) => isFolderLoading) &&
!Object.keys(foldersGroupedByEnv).length;
const isDashboardEmpty = isFoldersEmtpy && isDashboardSecretEmpty;
return ( return (
<div className="container mx-auto max-w-full px-6 text-mineshaft-50 dark:[color-scheme:dark]"> <div className="container mx-auto max-w-full px-6 text-mineshaft-50 dark:[color-scheme:dark]">
<FormProvider {...method}> <div className="relative right-5">
<form autoComplete="off"> <NavHeader pageName={t('dashboard.title')} isProjectRelated />
{/* breadcrumb row */} </div>
<div className="relative right-5"> <div className="mt-6">
<NavHeader pageName={t('dashboard.title')} isProjectRelated /> <p className="text-3xl font-semibold text-bunker-100">Secrets Overview</p>
<p className="text-md text-bunker-300">
Inject your secrets using
<a
className="mx-1 text-primary/80 hover:text-primary"
href="https://infisical.com/docs/cli/overview"
target="_blank"
rel="noopener noreferrer"
>
Infisical CLI
</a>
or
<a
className="mx-1 text-primary/80 hover:text-primary"
href="https://infisical.com/docs/sdks/overview"
target="_blank"
rel="noopener noreferrer"
>
Infisical SDKs
</a>
</p>
</div>
<div className="mt-8 flex items-center justify-between">
<div className="flex items-center space-x-2">
<div
className="breadcrumb relative z-20 border-solid border-mineshaft-600 bg-mineshaft-800 hover:bg-mineshaft-600 py-1 pl-5 pr-2 text-sm"
onClick={() => onFolderCrumbClick(0)}
onKeyDown={() => null}
role="button"
tabIndex={0}
>
<FontAwesomeIcon icon={faFolderOpen} className="text-primary" />
</div> </div>
<div className="mt-6"> {(secretPath || '')
<p className="text-3xl font-semibold text-bunker-100">Secrets Overview</p> .split('/')
<p className="text-md text-bunker-300"> .filter(Boolean)
Inject your secrets using .map((path, index, arr) => (
<a <div
className="mx-1 text-primary/80 hover:text-primary" key={`secret-path-${index + 1}`}
href="https://infisical.com/docs/cli/overview" className={`breadcrumb relative z-20 ${
target="_blank" index + 1 === arr.length ? 'cursor-default' : 'cursor-pointer'
rel="noopener noreferrer" } border-solid border-mineshaft-600 py-1 pl-5 pr-2 text-sm text-mineshaft-200`}
onClick={() => onFolderCrumbClick(index + 1)}
onKeyDown={() => null}
role="button"
tabIndex={0}
> >
Infisical CLI {path}
</a>
or
<a
className="mx-1 text-primary/80 hover:text-primary"
href="https://infisical.com/docs/sdks/overview"
target="_blank"
rel="noopener noreferrer"
>
Infisical SDKs
</a>
</p>
</div>
<div className="absolute top-[11.1rem] right-6 flex w-full max-w-sm flex-grow space-x-2">
<Input
className="h-[2.3rem] bg-mineshaft-800 placeholder-mineshaft-50 duration-200 focus:bg-mineshaft-700/80"
placeholder="Search by secret name..."
value={searchFilter}
onChange={(e) => setSearchFilter(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
/>
</div>
<div className="overflow-y-auto">
<div className="sticky top-0 mt-8 flex h-10 min-w-[60.3rem] flex-row rounded-md border border-mineshaft-600 bg-mineshaft-800">
<div className="sticky top-0 flex w-10 items-center justify-center border-none px-4">
<div className="w-10 text-center text-xs text-transparent">{0}</div>
</div> </div>
<div className="sticky top-0 border-none"> ))}
<div className="relative flex h-full w-full min-w-[200px] items-center justify-start lg:min-w-[220px] xl:min-w-[250px]"> </div>
<div className="text-sm font-medium ">Secret</div> <div className="w-80">
</div> <Input
</div> className="h-[2.3rem] bg-mineshaft-800 placeholder-mineshaft-50 duration-200 focus:bg-mineshaft-700/80"
{numSecretsMissingPerEnv && placeholder="Search by secret/folder name..."
userAvailableEnvs?.map((env) => { value={searchFilter}
return ( onChange={(e) => setSearchFilter(e.target.value)}
<div leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
key={`header-${env.slug}`} />
className="flex w-full min-w-[11rem] flex-row items-center rounded-md border-none bg-mineshaft-800" </div>
> </div>
<div className="flex w-full flex-row justify-center text-center text-sm font-medium text-bunker-200/[.99]"> <div className="overflow-y-auto">
{env.name} <div className="sticky top-0 mt-3 flex h-10 min-w-[60.3rem] flex-row rounded-md border border-mineshaft-600 bg-mineshaft-800">
{numSecretsMissingPerEnv[env.slug] > 0 && ( <div className="sticky top-0 flex w-10 items-center justify-center border-none px-4">
<div className="mt-0.5 ml-2.5 flex h-[1.1rem] w-[1.1rem] cursor-default items-center justify-center rounded-sm border border-red-400 bg-red text-xs text-bunker-100"> <div className="w-10 text-center text-xs text-transparent">{0}</div>
<Tooltip </div>
content={`${ <div className="sticky top-0 border-none">
numSecretsMissingPerEnv[env.slug] <div className="relative flex h-full w-full min-w-[200px] items-center justify-start lg:min-w-[220px] xl:min-w-[250px]">
} secrets missing compared to other environments`} <div className="text-sm font-medium ">Secret</div>
> </div>
<span className="text-bunker-100"> </div>
{numSecretsMissingPerEnv[env.slug]} {numSecretsMissingPerEnv &&
</span> userAvailableEnvs?.map((env) => {
</Tooltip> return (
</div> <div
)} key={`header-${env.slug}`}
className="flex w-full min-w-[11rem] flex-row items-center rounded-md border-none bg-mineshaft-800"
>
<div className="flex w-full flex-row justify-center text-center text-sm font-medium text-bunker-200/[.99]">
{env.name}
{numSecretsMissingPerEnv[env.slug] > 0 && (
<div className="mt-0.5 ml-2.5 flex h-[1.1rem] w-[1.1rem] cursor-default items-center justify-center rounded-sm border border-red-400 bg-red text-xs text-bunker-100">
<Tooltip
content={`${
numSecretsMissingPerEnv[env.slug]
} secrets missing compared to other environments`}
>
<span className="text-bunker-100">
{numSecretsMissingPerEnv[env.slug]}
</span>
</Tooltip>
</div> </div>
</div> )}
);
})}
</div>
<div
className={`${
isDashboardSecretEmpty ? '' : ''
} no-scrollbar::-webkit-scrollbar mt-3 flex h-full max-h-[calc(100vh-370px)] w-full min-w-[60.3rem] flex-grow flex-row items-start justify-center overflow-x-hidden rounded-md border border-mineshaft-600 no-scrollbar`}
>
{!isDashboardSecretEmpty && (
<TableContainer className="border-none">
<table className="secret-table relative w-full bg-mineshaft-900">
<tbody className="max-h-screen overflow-y-auto">
{Object.keys(secrets?.secrets || {})?.filter((secret: any) => secret.toUpperCase().includes(searchFilter.toUpperCase())).map((key, index) => (
<EnvComparisonRow
key={`row-${key}`}
secrets={secrets?.secrets?.[key]}
isReadOnly={isReadOnly}
index={index}
isSecretValueHidden
userAvailableEnvs={userAvailableEnvs}
/>
))}
</tbody>
</table>
</TableContainer>
)}
{isDashboardSecretEmpty && (
<div className="flex h-40 w-full flex-row rounded-md">
<div className="flex w-full min-w-[11rem] flex-col items-center justify-center rounded-md border-none bg-mineshaft-800 text-bunker-300">
<FontAwesomeIcon icon={faKey} className="text-4xl mb-4" />
<span className="mb-1">No secrets found.</span>
<span>To add more secrets you can explore any environment.</span>
</div> </div>
</div> </div>
)} );
{/* In future, we should add an option to add environments here })}
<div className="flex items-start justify-center h-full ml-10"> </div>
<Button <div
leftIcon={<FontAwesomeIcon icon={faPlus}/>} className={`${
onClick={() => prepend(DEFAULT_SECRET_VALUE, { shouldFocus: false })} isDashboardEmpty ? '' : ''
variant="outline_bg" } no-scrollbar::-webkit-scrollbar mt-3 flex h-full max-h-[calc(100vh-370px)] w-full min-w-[60.3rem] flex-grow flex-row items-start justify-center overflow-x-hidden rounded-md border border-mineshaft-600 no-scrollbar`}
colorSchema="primary" >
isFullWidth {!isDashboardEmpty && (
className="h-10" <TableContainer className="border-none">
> <table className="secret-table relative w-full bg-mineshaft-900">
Add Environment <tbody className="max-h-screen overflow-y-auto">
</Button> {Object.keys(foldersGroupedByEnv || {}).map((folderName, index) => (
</div> */} <FolderComparisonRow
</div> key={`${folderName}-${index + 1}`}
<div className="group mt-4 flex min-w-[60.3rem] flex-row items-center"> folderName={folderName}
<div className="flex h-10 w-10 items-center justify-center border-none px-4"> userAvailableEnvs={userAvailableEnvs}
<div className="w-10 text-center text-xs text-transparent">0</div> folderInEnv={foldersGroupedByEnv[folderName]}
onClick={onFolderClick}
/>
))}
{Object.keys(secrets?.secrets || {})
?.filter((secret: any) =>
secret.toUpperCase().includes(searchFilter.toUpperCase())
)
.map((key) => (
<EnvComparisonRow
key={`row-${key}`}
secrets={secrets?.secrets?.[key]}
isReadOnly
isSecretValueHidden
userAvailableEnvs={userAvailableEnvs}
/>
))}
</tbody>
</table>
</TableContainer>
)}
{isDashboardEmpty && (
<div className="flex h-40 w-full flex-row rounded-md">
<div className="flex w-full min-w-[11rem] flex-col items-center justify-center rounded-md border-none bg-mineshaft-800 text-bunker-300">
<FontAwesomeIcon icon={faKey} className="mb-4 text-4xl" />
<span className="mb-1">No secrets/folders found.</span>
<span>To add more secrets you can explore any environment.</span>
</div> </div>
<div className="flex min-w-[200px] flex-row items-center justify-between lg:min-w-[220px] xl:min-w-[250px]">
<span className="text-transparent">0</span>
<button type="button" className="mr-2 text-transparent">
1
</button>
</div>
{userAvailableEnvs?.map((env) => {
return (
<div
key={`button-${env.slug}`}
className="mx-2 mb-1 flex h-10 w-full min-w-[11rem] flex-row items-center justify-center border-none"
>
<Button
onClick={() => onEnvChange(env.slug)}
// router.push(`${router.asPath }?env=${env.slug}`)
variant="outline_bg"
colorSchema="primary"
isFullWidth
className="h-10"
>
Explore {env.name}
</Button>
</div>
);
})}
</div> </div>
)}
</div>
<div className="group mt-4 flex min-w-[60.3rem] flex-row items-center">
<div className="flex h-10 w-10 items-center justify-center border-none px-4">
<div className="w-10 text-center text-xs text-transparent">0</div>
</div> </div>
</form> <div className="flex min-w-[200px] flex-row items-center justify-between lg:min-w-[220px] xl:min-w-[250px]">
</FormProvider> <span className="text-transparent">0</span>
<button type="button" className="mr-2 text-transparent">
1
</button>
</div>
{userAvailableEnvs?.map((env) => {
return (
<div
key={`button-${env.slug}`}
className="mx-2 mb-1 flex h-10 w-full min-w-[11rem] flex-row items-center justify-center border-none"
>
<Button
onClick={() => onExploreEnv(env.slug)}
variant="outline_bg"
colorSchema="primary"
isFullWidth
className="h-10"
>
Explore {env.name}
</Button>
</div>
);
})}
</div>
</div>
</div> </div>
); );
}; };

View File

@@ -1,11 +1,10 @@
/* eslint-disable react/jsx-no-useless-fragment */ /* eslint-disable react/jsx-no-useless-fragment */
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import { faCircle, faEye, faEyeSlash, faMinus } from '@fortawesome/free-solid-svg-icons'; import { faCircle, faEye, faEyeSlash, faKey, faMinus } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { twMerge } from 'tailwind-merge'; import { twMerge } from 'tailwind-merge';
type Props = { type Props = {
index: number;
secrets: any[] | undefined; secrets: any[] | undefined;
// permission and external state's that decided to hide or show // permission and external state's that decided to hide or show
isReadOnly?: boolean; isReadOnly?: boolean;
@@ -30,7 +29,7 @@ const DashboardInput = ({
if (val === undefined) if (val === undefined)
return ( return (
<span className="cursor-default font-sans text-xs italic text-red-500/80"> <span className="cursor-default font-sans text-xs italic text-red-500/80">
<FontAwesomeIcon icon={faMinus} className="mt-1" /> <FontAwesomeIcon icon={faMinus} className="mt-1" />
</span> </span>
); );
if (val?.length === 0) if (val?.length === 0)
@@ -110,7 +109,6 @@ const DashboardInput = ({
}; };
export const EnvComparisonRow = ({ export const EnvComparisonRow = ({
index,
secrets, secrets,
isSecretValueHidden, isSecretValueHidden,
isReadOnly, isReadOnly,
@@ -126,7 +124,9 @@ export const EnvComparisonRow = ({
return ( return (
<tr className="group flex min-w-full flex-row items-center hover:bg-mineshaft-800"> <tr className="group flex min-w-full flex-row items-center hover:bg-mineshaft-800">
<td className="flex h-10 w-10 items-center justify-center border-none px-4"> <td className="flex h-10 w-10 items-center justify-center border-none px-4">
<div className="w-10 text-center text-xs text-bunker-400">{index + 1}</div> <div className="w-10 text-center text-xs text-bunker-400">
<FontAwesomeIcon icon={faKey} />
</div>
</td> </td>
<td className="flex h-full min-w-[200px] flex-row items-center justify-between lg:min-w-[220px] xl:min-w-[250px]"> <td className="flex h-full min-w-[200px] flex-row items-center justify-between lg:min-w-[220px] xl:min-w-[250px]">
<div className="flex h-8 cursor-default flex-row items-center truncate"> <div className="flex h-8 cursor-default flex-row items-center truncate">

View File

@@ -0,0 +1,42 @@
import { faCheck, faFolder, faXmark } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
type Props = {
folderInEnv: Record<string, boolean>;
userAvailableEnvs?: Array<{ slug: string; name: string }>;
folderName: string;
onClick: (folderName: string) => void;
};
export const FolderComparisonRow = ({
folderInEnv = {},
userAvailableEnvs = [],
folderName,
onClick
}: Props) => (
<tr
className="group flex min-w-full cursor-pointer flex-row items-center hover:bg-mineshaft-800"
onClick={() => onClick(folderName)}
>
<td className="flex h-10 w-10 items-center justify-center border-none px-4">
<div className="w-10 text-center text-xs text-bunker-400">
<FontAwesomeIcon icon={faFolder} className="text-primary-700" />
</div>
</td>
<td className="flex h-full min-w-[200px] flex-row items-center justify-between lg:min-w-[200px] xl:min-w-[250px]">
<div className="flex h-8 flex-row items-center truncate">{folderName}</div>
</td>
{userAvailableEnvs?.map(({ slug }) => (
<td
className={`flex h-10 w-full cursor-default flex-row items-center justify-center ${
folderInEnv[slug]
? 'bg-mineshaft-900/30 text-green-500/80'
: 'bg-red-800/10 text-red-500/80'
}`}
key={`${folderName}-${slug}`}
>
<FontAwesomeIcon icon={folderInEnv[slug] ? faCheck : faXmark} />
</td>
))}
</tr>
);

View File

@@ -41,12 +41,11 @@ import {
CreateServiceToken, CreateServiceToken,
CreateUpdateEnvFormData, CreateUpdateEnvFormData,
CreateWsTag, CreateWsTag,
E2EESection,
EnvironmentSection, EnvironmentSection,
ProjectIndexSecretsSection, ProjectIndexSecretsSection,
ProjectNameChangeSection, ProjectNameChangeSection,
ServiceTokenSection, ServiceTokenSection} from './components';
E2EESection
} from './components';
export const ProjectSettingsPage = () => { export const ProjectSettingsPage = () => {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -90,8 +89,8 @@ export const ProjectSettingsPage = () => {
// get user subscription // get user subscription
const { subscription } = useSubscription(); const { subscription } = useSubscription();
const host = window.location.origin;
const isEnvServiceAllowed = ((currentWorkspace?.environments || []).length < (subscription?.envLimit || 3) || host !== 'https://app.infisical.com'); const isEnvServiceAllowed = (subscription?.environmentLimit && currentWorkspace?.environments) ? (currentWorkspace.environments.length < subscription.environmentLimit) : true;
const onRenameWorkspace = async (name: string) => { const onRenameWorkspace = async (name: string) => {
try { try {