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"
replicaCount: 2
image:
repository: infisical/frontend
repository: infisical/staging_deployment_frontend
tag: "latest"
pullPolicy: Always
kubeSecretRef: managed-secret-frontend
@@ -25,7 +25,7 @@ backend:
secrets.infisical.com/auto-reload: "true"
replicaCount: 2
image:
repository: infisical/backend
repository: infisical/staging_deployment_backend
tag: "latest"
pullPolicy: Always
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
on:
push:
tags:
- "infisical/v*.*.*"
on: [workflow_dispatch]
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
@@ -57,18 +51,14 @@ jobs:
push: true
context: backend
tags: |
infisical/backend:${{ steps.commit.outputs.short }}
infisical/backend:latest
infisical/backend:${{ steps.extract_version.outputs.version }}
infisical/staging_deployment_backend:${{ steps.commit.outputs.short }}
infisical/staging_deployment_backend:latest
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
@@ -90,12 +80,12 @@ jobs:
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
project: 64mmf0n610
context: frontend
tags: infisical/frontend:test
tags: infisical/staging_deployment_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
docker run -d --rm --name infisical-frontend-test infisical/staging_deployment_frontend:test
- name: 🧪 Test frontend image
run: |
./.github/resources/healthcheck.sh infisical-frontend-test
@@ -110,9 +100,8 @@ jobs:
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
context: frontend
tags: |
infisical/frontend:${{ steps.commit.outputs.short }}
infisical/frontend:latest
infisical/frontend:${{ steps.extract_version.outputs.version }}
infisical/staging_deployment_frontend:${{ steps.commit.outputs.short }}
infisical/staging_deployment_frontend:latest
platforms: linux/amd64,linux/arm64
build-args: |
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
@@ -146,7 +135,7 @@ jobs:
- name: Download helm values to file and upgrade gamma deploy
run: |
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
echo "Helm upgrade failed"
exit 1

View File

@@ -25,7 +25,7 @@
<img src="https://img.shields.io/github/commit-activity/m/infisical/infisical" alt="git commit activity" />
</a>
<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 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" />
@@ -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:
- [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.
## Resources

View File

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

View File

@@ -1,10 +1,11 @@
import { Request, Response } from 'express';
import { Types } from 'mongoose';
import {
Integration
} from '../../models';
import { EventService } from '../../services';
import { eventPushSecrets } from '../../events';
import { Request, Response } from "express";
import { Types } from "mongoose";
import { Integration } from "../../models";
import { EventService } from "../../services";
import { eventPushSecrets } from "../../events";
import Folder from "../../models/folder";
import { getFolderByPath } from "../../services/FolderService";
import { BadRequestError } from "../../utils/errors";
/**
* Create/initialize an (empty) integration for integration authorization
@@ -25,9 +26,24 @@ export const createIntegration = async (req: Request, res: Response) => {
targetServiceId,
owner,
path,
region
region,
secretPath,
} = 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]
// initialize new integration after saving integration access token
@@ -44,17 +60,18 @@ export const createIntegration = async (req: Request, res: Response) => {
owner,
path,
region,
secretPath,
integration: req.integrationAuth.integration,
integrationAuth: new Types.ObjectId(integrationAuthId)
integrationAuth: new Types.ObjectId(integrationAuthId),
}).save();
if (integration) {
// trigger event - push secrets
EventService.handleEvent({
event: eventPushSecrets({
workspaceId: integration.workspace,
environment: sourceEnvironment
})
environment: sourceEnvironment,
}),
});
}
@@ -70,7 +87,6 @@ export const createIntegration = async (req: Request, res: Response) => {
* @returns
*/
export const updateIntegration = async (req: Request, res: Response) => {
// TODO: add integration-specific validation to ensure that each
// integration has the correct fields populated in [Integration]
@@ -81,8 +97,23 @@ export const updateIntegration = async (req: Request, res: Response) => {
appId,
targetEnvironment,
owner, // github-specific integration param
secretPath,
} = 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(
{
_id: req.integration._id,
@@ -94,6 +125,7 @@ export const updateIntegration = async (req: Request, res: Response) => {
appId,
targetEnvironment,
owner,
secretPath,
},
{
new: true,
@@ -105,7 +137,7 @@ export const updateIntegration = async (req: Request, res: Response) => {
EventService.handleEvent({
event: eventPushSecrets({
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');
}
const plan = await EELicenseService.getOrganizationPlan(organizationId);
const plan = await EELicenseService.getPlan(organizationId);
if (plan.memberLimit !== null) {
// 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) {
// case: user has already completed account
return res.status(403).send({
message: "If an account exists with this email, a password reset link has been sent"
return res.status(200).send({
message:"If an account exists with this email, a password reset link has been sent"
});
}

View File

@@ -11,6 +11,7 @@ import {
validateFolderName,
generateFolderId,
getParentFromFolderId,
getFolderByPath,
} from "../../services/FolderService";
import { ADMIN, MEMBER } from "../../variables";
import { validateMembership } from "../../helpers/membership";
@@ -177,11 +178,13 @@ export const deleteFolder = async (req: Request, res: Response) => {
// TODO: validate workspace
export const getFolders = async (req: Request, res: Response) => {
const { workspaceId, environment, parentFolderId } = req.query as {
workspaceId: string;
environment: string;
parentFolderId?: string;
};
const { workspaceId, environment, parentFolderId, parentFolderPath } =
req.query as {
workspaceId: string;
environment: string;
parentFolderId?: string;
parentFolderPath?: string;
};
const folders = await Folder.findOne({ workspace: workspaceId, environment });
if (!folders) {
@@ -196,6 +199,20 @@ export const getFolders = async (req: Request, res: Response) => {
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) {
const rootFolders = folders.nodes.children.map(({ id, name }) => ({
id,

View File

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

View File

@@ -8,7 +8,8 @@ import {
Membership,
} from '../../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 { PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS } from '../../variables';
@@ -22,9 +23,26 @@ export const createWorkspaceEnvironment = async (
req: Request,
res: Response
) => {
const { workspaceId } = req.params;
const { environmentName, environmentSlug } = req.body;
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 (
!workspace ||
workspace?.environments.find(
@@ -40,6 +58,8 @@ export const createWorkspaceEnvironment = async (
});
await workspace.save();
await EELicenseService.refreshPlan(workspace.organization.toString(), workspaceId);
return res.status(200).send({
message: 'Successfully created new environment',
workspace: workspaceId,
@@ -186,7 +206,9 @@ export const deleteWorkspaceEnvironment = async (
await Membership.updateMany(
{ workspace: workspaceId },
{ $pull: { deniedPermissions: { environmentSlug: environmentSlug } } }
)
);
await EELicenseService.refreshPlan(workspace.organization.toString(), workspaceId);
return res.status(200).send({
message: 'Successfully deleted environment',

View File

@@ -700,11 +700,15 @@ export const getSecrets = async (req: Request, res: Response) => {
(!folders && folderId && folderId !== "root") ||
(!folders && secretPath)
) {
throw BadRequestError({ message: "Folder not found" });
res.send({ secrets: [] });
return;
}
if (folders && folderId !== "root") {
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) {
@@ -720,10 +724,11 @@ export const getSecrets = async (req: Request, res: Response) => {
}
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);
if (!folder) {
throw BadRequestError({ message: "Secret path not found" });
res.send({ secrets: [] });
return;
}
folderId = folder.id;
}

View File

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

View File

@@ -8,8 +8,9 @@ import { EELicenseService } from '../../services';
*/
export const getOrganizationPlan = async (req: Request, res: Response) => {
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({
plan,

View File

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

View File

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

View File

@@ -1,29 +1,21 @@
import { Types } from "mongoose";
import {
Bot,
BotKey,
Secret,
ISecret,
IUser
} from "../models";
import { Bot, BotKey, Secret, ISecret, IUser } from "../models";
import {
generateKeyPair,
encryptSymmetric128BitHexKeyUTF8,
decryptSymmetric128BitHexKeyUTF8,
decryptAsymmetric
} from '../utils/crypto';
decryptAsymmetric,
} from "../utils/crypto";
import {
SECRET_SHARED,
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_UTF8,
ENCODING_SCHEME_BASE64
ENCODING_SCHEME_BASE64,
} from "../variables";
import {
getEncryptionKey,
getRootEncryptionKey,
client
} from "../config";
import { getEncryptionKey, getRootEncryptionKey, client } from "../config";
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]
@@ -40,15 +32,14 @@ export const createBot = async ({
}) => {
const encryptionKey = await getEncryptionKey();
const rootEncryptionKey = await getRootEncryptionKey();
const { publicKey, privateKey } = generateKeyPair();
if (rootEncryptionKey) {
const {
ciphertext,
iv,
tag
} = client.encryptSymmetric(privateKey, rootEncryptionKey);
const { ciphertext, iv, tag } = client.encryptSymmetric(
privateKey,
rootEncryptionKey
);
return await new Bot({
name,
@@ -59,9 +50,8 @@ export const createBot = async ({
iv,
tag,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_BASE64
keyEncoding: ENCODING_SCHEME_BASE64,
}).save();
} else if (encryptionKey) {
const { ciphertext, iv, tag } = encryptSymmetric128BitHexKeyUTF8({
plaintext: privateKey,
@@ -77,12 +67,12 @@ export const createBot = async ({
iv,
tag,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
keyEncoding: ENCODING_SCHEME_UTF8,
}).save();
}
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) => {
const botKey = await BotKey.exists({
workspace: workspaceId
});
workspace: workspaceId,
});
return botKey ? false : true;
}
};
/**
* Return decrypted secrets for workspace with id [workspaceId]
@@ -108,16 +98,38 @@ export const getIsWorkspaceE2EEHelper = async (workspaceId: Types.ObjectId) => {
export const getSecretsBotHelper = async ({
workspaceId,
environment,
secretPath,
}: {
workspaceId: Types.ObjectId;
environment: string;
secretPath: string;
}) => {
const content = {} as any;
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({
workspace: workspaceId,
environment,
type: SECRET_SHARED,
folder: folderId,
});
secrets.forEach((secret: ISecret) => {
@@ -148,14 +160,17 @@ export const getSecretsBotHelper = async ({
* @param {String} obj.workspaceId - id of workspace
* @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 rootEncryptionKey = await getRootEncryptionKey();
const botKey = await BotKey.findOne({
workspace: workspaceId,
})
.populate<{ sender: IUser }>("sender", "publicKey");
}).populate<{ sender: IUser }>("sender", "publicKey");
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) {
// 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({
ciphertext: botKey.encryptedKey,
@@ -177,15 +197,14 @@ export const getKey = async ({ workspaceId }: { workspaceId: Types.ObjectId }) =
privateKey: privateKeyBot,
});
} else if (encryptionKey && bot.keyEncoding === ENCODING_SCHEME_UTF8) {
// case: encoding scheme is utf8
const privateKeyBot = decryptSymmetric128BitHexKeyUTF8({
ciphertext: bot.encryptedPrivateKey,
iv: bot.iv,
tag: bot.tag,
key: encryptionKey
key: encryptionKey,
});
return decryptAsymmetric({
ciphertext: botKey.encryptedKey,
nonce: botKey.nonce,
@@ -195,7 +214,8 @@ export const getKey = async ({ workspaceId }: { workspaceId: Types.ObjectId }) =
}
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;
};
};

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 {
Bot,
Integration,
IntegrationAuth
} from '../models';
import { exchangeCode, exchangeRefresh, syncSecrets } from '../integrations';
import { BotService } from '../services';
import {
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_UTF8
} from '../variables';
import {
UnauthorizedRequestError,
} from '../utils/errors';
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_UTF8,
} from "../variables";
import { UnauthorizedRequestError } from "../utils/errors";
interface Update {
workspace: string;
integration: string;
teamId?: string;
accountId?: string;
workspace: string;
integration: string;
teamId?: string;
accountId?: string;
}
/**
@@ -31,78 +25,83 @@ interface Update {
* - Create bot sequence for integration
* @param {Object} obj
* @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
* @returns {IntegrationAuth} integrationAuth - integration auth after OAuth2 code-token exchange
*/
*/
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,
code,
environment
}: {
workspaceId: string;
integration: string;
code: string;
environment: string;
}) => {
const bot = await Bot.findOne({
workspace: workspaceId,
isActive: true
});
const update: Update = {
workspace: workspaceId,
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 (!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,
code
}
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,
});
const update: Update = {
workspace: workspaceId,
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;
}
}
return integrationAuth;
};
/**
* Sync/push environment variables in workspace with id [workspaceId] to
* all active integrations for that workspace
@@ -110,48 +109,54 @@ export const handleOAuthExchangeHelper = async ({
* @param {Object} obj.workspaceId - id of workspace
*/
export const syncIntegrationsHelper = async ({
workspaceId,
environment
workspaceId,
environment,
}: {
workspaceId: Types.ObjectId;
environment?: string;
workspaceId: Types.ObjectId;
environment?: string;
}) => {
const integrations = await Integration.find({
workspace: workspaceId,
...(environment ? {
environment
} : {}),
isActive: true,
app: { $ne: null }
const integrations = await Integration.find({
workspace: workspaceId,
...(environment
? {
environment,
}
: {}),
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
// 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
});
const integrationAuth = await IntegrationAuth.findById(
integration.integrationAuth
);
if (!integrationAuth) throw new Error("Failed to find integration auth");
const integrationAuth = await IntegrationAuth.findById(integration.integrationAuth);
if (!integrationAuth) throw new Error('Failed to find integration auth');
// get integration auth access token
const access = await getIntegrationAuthAccessHelper({
integrationAuthId: integration.integrationAuth
});
// get integration auth access token
const access = await getIntegrationAuthAccessHelper({
integrationAuthId: integration.integrationAuth,
});
// sync secrets to integration
await syncSecrets({
integration,
integrationAuth,
secrets,
accessId: access.accessId === undefined ? null : access.accessId,
accessToken: access.accessToken
});
}
}
// sync secrets to integration
await syncSecrets({
integration,
integrationAuth,
secrets,
accessId: access.accessId === undefined ? null : access.accessId,
accessToken: access.accessToken,
});
}
};
/**
* 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} refreshToken - decrypted refresh token
*/
export const getIntegrationAuthRefreshHelper = async ({ integrationAuthId }: { integrationAuthId: Types.ObjectId }) => {
const integrationAuth = await IntegrationAuth
.findById(integrationAuthId)
.select('+refreshCiphertext +refreshIV +refreshTag');
export const getIntegrationAuthRefreshHelper = async ({
integrationAuthId,
}: {
integrationAuthId: Types.ObjectId;
}) => {
const integrationAuth = await IntegrationAuth.findById(
integrationAuthId
).select("+refreshCiphertext +refreshIV +refreshTag");
if (!integrationAuth) throw UnauthorizedRequestError({message: 'Failed to locate Integration Authentication credentials'});
const refreshToken = await BotService.decryptSymmetric({
workspaceId: integrationAuth.workspace,
ciphertext: integrationAuth.refreshCiphertext as string,
iv: integrationAuth.refreshIV as string,
tag: integrationAuth.refreshTag as string
if (!integrationAuth)
throw UnauthorizedRequestError({
message: "Failed to locate Integration Authentication credentials",
});
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
@@ -186,50 +198,65 @@ export const getIntegrationAuthRefreshHelper = async ({ integrationAuthId }: { i
* @param {String} obj.integrationAuthId - id of integration auth
* @returns {String} accessToken - decrypted access token
*/
export const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrationAuthId: Types.ObjectId }) => {
let accessId;
let accessToken;
const integrationAuth = await IntegrationAuth
.findById(integrationAuthId)
.select('workspace integration +accessCiphertext +accessIV +accessTag +accessExpiresAt + refreshCiphertext +accessIdCiphertext +accessIdIV +accessIdTag');
export const getIntegrationAuthAccessHelper = async ({
integrationAuthId,
}: {
integrationAuthId: Types.ObjectId;
}) => {
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'});
accessToken = await BotService.decryptSymmetric({
workspaceId: integrationAuth.workspace,
ciphertext: integrationAuth.accessCiphertext as string,
iv: integrationAuth.accessIV as string,
tag: integrationAuth.accessTag as string
if (!integrationAuth)
throw UnauthorizedRequestError({
message: "Failed to locate Integration Authentication credentials",
});
if (integrationAuth?.accessExpiresAt && integrationAuth?.refreshCiphertext) {
// there is a access token expiration date
// 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
});
}
accessToken = await BotService.decryptSymmetric({
workspaceId: integrationAuth.workspace,
ciphertext: integrationAuth.accessCiphertext as string,
iv: integrationAuth.accessIV as string,
tag: integrationAuth.accessTag as string,
});
if (integrationAuth?.accessExpiresAt && integrationAuth?.refreshCiphertext) {
// there is a access token expiration date
// 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({
workspaceId: integrationAuth.workspace,
ciphertext: integrationAuth.accessIdCiphertext as string,
iv: integrationAuth.accessIdIV as string,
tag: integrationAuth.accessIdTag as string
});
}
return ({
accessId,
accessToken
}
if (
integrationAuth?.accessIdCiphertext &&
integrationAuth?.accessIdIV &&
integrationAuth?.accessIdTag
) {
accessId = await BotService.decryptSymmetric({
workspaceId: integrationAuth.workspace,
ciphertext: integrationAuth.accessIdCiphertext as string,
iv: integrationAuth.accessIdIV as string,
tag: integrationAuth.accessIdTag as string,
});
}
}
return {
accessId,
accessToken,
};
};
/**
* 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
*/
export const setIntegrationAuthRefreshHelper = async ({
integrationAuthId,
refreshToken
integrationAuthId,
refreshToken,
}: {
integrationAuthId: string;
refreshToken: string;
integrationAuthId: string;
refreshToken: string;
}) => {
let integrationAuth = await IntegrationAuth
.findById(integrationAuthId);
if (!integrationAuth) throw new Error('Failed to find integration auth');
const obj = await BotService.encryptSymmetric({
workspaceId: integrationAuth.workspace,
plaintext: refreshToken
});
integrationAuth = await IntegrationAuth.findOneAndUpdate({
_id: integrationAuthId
}, {
refreshCiphertext: obj.ciphertext,
refreshIV: obj.iv,
refreshTag: obj.tag,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
}, {
new: true
});
return integrationAuth;
}
let integrationAuth = await IntegrationAuth.findById(integrationAuthId);
if (!integrationAuth) throw new Error("Failed to find integration auth");
const obj = await BotService.encryptSymmetric({
workspaceId: integrationAuth.workspace,
plaintext: refreshToken,
});
integrationAuth = await IntegrationAuth.findOneAndUpdate(
{
_id: integrationAuthId,
},
{
refreshCiphertext: obj.ciphertext,
refreshIV: obj.iv,
refreshTag: obj.tag,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8,
},
{
new: true,
}
);
return integrationAuth;
};
/**
* 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]
* @param {Object} obj
* @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
*/
export const setIntegrationAuthAccessHelper = async ({
integrationAuthId,
accessId,
accessToken,
accessExpiresAt
integrationAuthId,
accessId,
accessToken,
accessExpiresAt,
}: {
integrationAuthId: string;
accessId: string | null;
accessToken: string;
accessExpiresAt: Date | undefined;
integrationAuthId: string;
accessId: string | null;
accessToken: string;
accessExpiresAt: Date | undefined;
}) => {
let integrationAuth = await IntegrationAuth.findById(integrationAuthId);
if (!integrationAuth) throw new Error('Failed to find integration auth');
const encryptedAccessTokenObj = await BotService.encryptSymmetric({
workspaceId: integrationAuth.workspace,
plaintext: accessToken
let integrationAuth = await IntegrationAuth.findById(integrationAuthId);
if (!integrationAuth) throw new Error("Failed to find integration auth");
const encryptedAccessTokenObj = await BotService.encryptSymmetric({
workspaceId: integrationAuth.workspace,
plaintext: accessToken,
});
let encryptedAccessIdObj;
if (accessId) {
encryptedAccessIdObj = await BotService.encryptSymmetric({
workspaceId: integrationAuth.workspace,
plaintext: accessId,
});
let encryptedAccessIdObj;
if (accessId) {
encryptedAccessIdObj = await BotService.encryptSymmetric({
workspaceId: integrationAuth.workspace,
plaintext: accessId
});
}
integrationAuth = await IntegrationAuth.findOneAndUpdate(
{
_id: integrationAuthId,
},
{
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
}, {
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;
}
);
return integrationAuth;
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -13,6 +13,7 @@ import {
BackupPrivateKey,
IntegrationAuth,
ServiceTokenData,
Integration,
} from "../../models";
import { generateKeyPair } from "../../utils/crypto";
import { client, getEncryptionKey, getRootEncryptionKey } from "../../config";
@@ -424,3 +425,19 @@ export const backfillServiceToken = async () => {
);
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,
backfillSecretFolders,
backfillServiceToken,
backfillIntegration,
} from "./backfillData";
import {
reencryptBotPrivateKeys,
@@ -77,6 +78,7 @@ export const setup = async () => {
await backfillEncryptionMetadata();
await backfillSecretFolders();
await backfillServiceToken();
await backfillIntegration();
// re-encrypt any data previously encrypted under server hex 128-bit ENCRYPTION_KEY
// to base64 256-bit ROOT_ENCRYPTION_KEY

View File

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

View File

@@ -2,6 +2,7 @@ package api
import (
"fmt"
"net/http"
"github.com/Infisical/infisical-merge/packages/config"
"github.com/go-resty/resty/v2"
@@ -10,63 +11,6 @@ import (
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) {
endpoint := fmt.Sprintf("%v/v2/workspace/%v/encrypted-key", config.INFISICAL_URL, request.WorkspaceId)
var result GetEncryptedWorkspaceKeyResponse
@@ -106,28 +50,6 @@ func CallGetServiceTokenDetailsV2(httpClient *resty.Client) (GetServiceTokenDeta
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) {
var loginOneV2Response GetLoginOneV2Response
response, err := httpClient.
@@ -159,6 +81,22 @@ func CallVerifyMfaToken(httpClient *resty.Client, request VerifyMfaTokenRequest)
SetBody(request).
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 {
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).
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 {
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
}
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"`
Type string `json:"type,omitempty"`
ID string `json:"id,omitempty"`
}
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"`
PlainTextKey string `json:"plainTextKey"`
}
type GetEncryptedWorkspaceKeyRequest struct {
@@ -194,41 +177,6 @@ type GetSecretsByWorkspaceIdAndEnvironmentRequest struct {
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 {
ID string `json:"_id"`
Name string `json:"name"`
@@ -281,6 +229,7 @@ type GetLoginTwoV2Response struct {
ProtectedKey string `json:"protectedKey"`
ProtectedKeyIV string `json:"protectedKeyIV"`
ProtectedKeyTag string `json:"protectedKeyTag"`
RefreshToken string `json:"RefreshToken"`
}
type VerifyMfaTokenRequest struct {
@@ -298,6 +247,7 @@ type VerifyMfaTokenResponse struct {
ProtectedKey string `json:"protectedKey"`
ProtectedKeyIV string `json:"protectedKeyIV"`
ProtectedKeyTag string `json:"protectedKeyTag"`
RefreshToken string `json:"refreshToken"`
}
type VerifyMfaTokenErrorResponse struct {
@@ -314,3 +264,113 @@ type VerifyMfaTokenErrorResponse struct {
Application string `json:"application"`
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)
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)
return
}
@@ -143,7 +143,7 @@ var loginCmd = &cobra.Command{
loginTwoResponse.Tag = verifyMFAresponse.Tag
loginTwoResponse.Token = verifyMFAresponse.Token
loginTwoResponse.EncryptionVersion = verifyMFAresponse.EncryptionVersion
loginTwoResponse.RefreshToken = verifyMFAresponse.RefreshToken
break
}
}
@@ -244,9 +244,10 @@ var loginCmd = &cobra.Command{
}
userCredentialsToBeStored := &models.UserCredentials{
Email: email,
PrivateKey: string(decryptedPrivateKey),
JTWToken: loginTwoResponse.Token,
Email: email,
PrivateKey: string(decryptedPrivateKey),
JTWToken: loginTwoResponse.Token,
RefreshToken: loginTwoResponse.RefreshToken,
}
err = util.StoreUserCredsInKeyRing(userCredentialsToBeStored)
@@ -414,7 +415,7 @@ func getFreshUserCredentials(email string, password string) (*api.GetLoginOneV2R
})
if err != nil {
util.HandleError(err)
return nil, nil, err
}
// **** Login 2

View File

@@ -82,7 +82,12 @@ var runCmd = &cobra.Command{
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 {
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().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().String("path", "/", "get secrets within a folder path")
}
// 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")
}
secretsPath, err := cmd.Flags().GetString("path")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
shouldExpandSecrets, err := cmd.Flags().GetBool("expand")
if err != nil {
util.HandleError(err)
@@ -54,7 +59,7 @@ var secretsCmd = &cobra.Command{
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 {
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()
if err != nil {
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)
// pull current secrets
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName})
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, SecretsPath: secretsPath})
if err != nil {
util.HandleError(err, "unable to retrieve secrets")
}
@@ -191,6 +201,8 @@ var secretsSetCmd = &cobra.Command{
SecretValueIV: base64.StdEncoding.EncodeToString(encryptedValue.Nonce),
SecretValueTag: base64.StdEncoding.EncodeToString(encryptedValue.AuthTag),
SecretValueHash: hashedValue,
PlainTextKey: key,
Type: existingSecret.Type,
}
// Only add to modifications if the value is different
@@ -222,6 +234,7 @@ var secretsSetCmd = &cobra.Command{
SecretValueTag: base64.StdEncoding.EncodeToString(encryptedValue.AuthTag),
SecretValueHash: hashedValue,
Type: util.SECRET_TYPE_SHARED,
PlainTextKey: key,
}
secretsToCreate = append(secretsToCreate, encryptedSecretDetails)
secretOperations = append(secretOperations, SecretSetOperation{
@@ -232,30 +245,43 @@ var secretsSetCmd = &cobra.Command{
}
}
if len(secretsToCreate) > 0 {
batchCreateRequest := api.BatchCreateSecretsByWorkspaceAndEnvRequest{
WorkspaceId: workspaceFile.WorkspaceId,
Environment: environmentName,
Secrets: secretsToCreate,
for _, secret := range secretsToCreate {
createSecretRequest := api.CreateSecretV3Request{
WorkspaceID: workspaceFile.WorkspaceId,
Environment: environmentName,
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 {
util.HandleError(err, "Unable to process new secret creations")
return
}
}
if len(secretsToModify) > 0 {
batchModifyRequest := api.BatchModifySecretsByWorkspaceAndEnvRequest{
WorkspaceId: workspaceFile.WorkspaceId,
Environment: environmentName,
Secrets: secretsToModify,
for _, secret := range secretsToModify {
updateSecretRequest := api.UpdateSecretByNameV3Request{
WorkspaceID: workspaceFile.WorkspaceId,
Environment: environmentName,
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 {
util.HandleError(err, "Unable to process the modifications to your secrets")
util.HandleError(err, "Unable to process secret update request")
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()
if err != nil {
util.HandleError(err, "Unable to authenticate")
@@ -298,46 +334,28 @@ var secretsDeleteCmd = &cobra.Command{
util.HandleError(err, "Unable to get local project details")
}
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName})
if err != nil {
util.HandleError(err, "Unable to fetch secrets")
}
secretByKey := getSecretsByKeys(secrets)
validSecretIdsToDelete := []string{}
invalidSecretNamesThatDoNotExist := []string{}
for _, secretKeyFromArg := range args {
if value, ok := secretByKey[strings.ToUpper(secretKeyFromArg)]; ok {
validSecretIdsToDelete = append(validSecretIdsToDelete, value.ID)
} else {
invalidSecretNamesThatDoNotExist = append(invalidSecretNamesThatDoNotExist, secretKeyFromArg)
for _, secretName := range args {
request := api.DeleteSecretV3Request{
WorkspaceId: workspaceFile.WorkspaceId,
Environment: environmentName,
SecretName: secretName,
Type: secretType,
SecretPath: secretsPath,
}
}
if len(invalidSecretNamesThatDoNotExist) != 0 {
message := fmt.Sprintf("secret name(s) [%v] does not exist in your project. To see which secrets exist run [infisical secrets]", strings.Join(invalidSecretNamesThatDoNotExist, ", "))
util.PrintErrorMessageAndExit(message)
}
httpClient := resty.New().
SetAuthToken(loggedInUserDetails.UserCredentials.JTWToken).
SetHeader("Accept", "application/json")
request := api.BatchDeleteSecretsBySecretIdsRequest{
WorkspaceId: workspaceFile.WorkspaceId,
EnvironmentName: environmentName,
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")
err = api.CallDeleteSecretsV3(httpClient, request)
if err != nil {
util.HandleError(err, "Unable to complete your delete request")
}
}
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(secretsSetCmd)
secretsSetCmd.Flags().String("path", "/", "get secrets within a folder path")
secretsSetCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
util.RequireLogin()
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)
secretsDeleteCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
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.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets")
secretsCmd.PersistentFlags().StringP("tags", "t", "", "filter secrets by tag slugs")
secretsCmd.Flags().String("path", "/", "get secrets within a folder path")
rootCmd.AddCommand(secretsCmd)
}

View File

@@ -5,9 +5,10 @@ import (
)
type UserCredentials struct {
Email string `json:"email"`
PrivateKey string `json:"privateKey"`
JTWToken string `json:"JTWToken"`
Email string `json:"email"`
PrivateKey string `json:"privateKey"`
JTWToken string `json:"JTWToken"`
RefreshToken string `json:"RefreshToken"`
}
// The file struct for Infisical config file
@@ -63,4 +64,5 @@ type GetAllSecretsParameters struct {
InfisicalToken string
TagSlugs 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/models"
"github.com/go-resty/resty/v2"
"github.com/rs/zerolog/log"
)
type LoggedInUserDetails struct {
@@ -96,6 +97,20 @@ func GetCurrentLoggedInUserDetails() (LoggedInUserDetails, error) {
}
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 {
return LoggedInUserDetails{
IsUserLoggedIn: true, // was logged in

View File

@@ -74,23 +74,12 @@ func ConfigContainsEmail(users []models.LoggedInUser, email string) bool {
}
func RequireLogin() {
currentUserDetails, err := GetCurrentLoggedInUserDetails()
// get the config file that stores the current logged in user email
configFile, _ := GetConfigFile()
if err != nil {
HandleError(err, "unable to retrieve your login details")
}
if !currentUserDetails.IsUserLoggedIn {
if configFile.LoggedInUserEmail == "" {
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() {

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)
}
encryptedSecrets, err := api.CallGetSecretsV2(httpClient, api.GetEncryptedSecretsV2Request{
encryptedSecrets, err := api.CallGetSecretsV3(httpClient, api.GetEncryptedSecretsV3Request{
WorkspaceId: serviceTokenDetails.Workspace,
Environment: serviceTokenDetails.Environment,
})
@@ -61,7 +61,7 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string) ([]models.Singl
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.SetAuthToken(JTWToken).
SetHeader("Accept", "application/json")
@@ -102,11 +102,17 @@ func GetPlainTextSecretsViaJTW(JTWToken string, receiversPrivateKey string, work
plainTextWorkspaceKey := crypto.DecryptAsymmetric(encryptedWorkspaceKey, encryptedWorkspaceKeyNonce, encryptedWorkspaceKeySenderPublicKey, currentUsersPrivateKey)
encryptedSecrets, err := api.CallGetSecretsV2(httpClient, api.GetEncryptedSecretsV2Request{
getSecretsRequest := api.GetEncryptedSecretsV3Request{
WorkspaceId: workspaceId,
Environment: environmentName,
TagSlugs: tagSlugs,
})
// TagSlugs: tagSlugs,
}
if secretsPath != "" {
getSecretsRequest.SecretPath = secretsPath
}
encryptedSecrets, err := api.CallGetSecretsV3(httpClient, getSecretsRequest)
if err != nil {
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)
}
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)
backupSecretsEncryptionKey := []byte(loggedInUserDetails.UserCredentials.PrivateKey)[0:32]
@@ -333,7 +339,7 @@ func OverrideSecrets(secrets []models.SingleEnvironmentVariable, secretType stri
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{}
for _, secret := range encryptedSecrets.Secrets {
// Decrypt key

View File

@@ -3,32 +3,29 @@ title: "Authentication"
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.
- API Key: Provides full access to all endpoints representing the user without ability to encrypt/decrypt secrets for **E2EE** endpoints.
<AccordionGroup>
<Accordion title="API Key">
The API key mode uses an API key to authenticate with the API.
<Tabs>
<Tab title="Infisical Token">
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)
</Accordion>
<Accordion title="Infisical Token">
![token add](../../images/project-token-add.png)
</Tab>
<Tab title="API Key">
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.
![token add](../../images/project-token-add.png)
</Accordion>
</AccordionGroup>
![API key dashboard](../../images/api-key-dashboard.png)
![API key in personal settings](../../images/api-key-settings.png)
</Tab>
</Tabs>

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
when reading/writing secrets via HTTP call to Infisical.
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:
Using Infisical's API to read/write secrets with E2EE enabled allows you to create, update, and retrieve secrets
but requires you to perform client-side encryption/decryption operations. For this reason, we recommend using one of the available
SDKs instead.
<AccordionGroup>
<Accordion title="Retrieve secrets">
<Tabs>
<Tab title="Javascript">
Retrieve all secrets for an Infisical project and environment.
```js
const crypto = require('crypto');
const axios = require('axios');
@@ -194,6 +187,7 @@ get_secrets()
<Accordion title="Create secret">
<Tabs>
<Tab title="Javascript">
Create a secret in Infisical.
```js
const crypto = require('crypto');
const axios = require('axios');
@@ -408,6 +402,7 @@ create_secrets()
<Accordion title="Retrieve secret">
<Tabs>
<Tab title="Javascript">
Retrieve a secret from Infisical.
```js
const crypto = require('crypto');
const axios = require('axios');
@@ -569,6 +564,7 @@ get_secret()
<Accordion title="Update secret">
<Tabs>
<Tab title="Javascript">
Update an existing secret in Infisical.
```js
const crypto = require('crypto');
const axios = require('axios');
@@ -779,6 +775,7 @@ update_secret()
<Accordion title="Delete secret">
<Tabs>
<Tab title="Javascript">
Delete a secret in Infisical.
```js
const axios = require('axios');
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
</Card>
<Card
href="/documentation/getting-started/api"
title="REST API"
icon="cloud"
color="#3775a9"
>
Fetch secrets via HTTP request
</Card>
</CardGroup>
## Resources

View File

@@ -1,34 +1,91 @@
---
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)
- [Install the CLI](/cli/overview)
## Prerequisites
## Initialize Infisical for your [Terraform](https://www.terraform.io/) project
- Basic understanding of Terraform
- Install [Terraform](https://www.terraform.io/downloads.html)
```bash
# navigate to the root of your of your project
cd /path/to/project
## Steps
# then initialize Infisical
infisical init
### 1. Define Required Providers
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
infisical run -- <your application start command>
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.
# Example
infisical run -- terraform plan
```hcl main.tf
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>
To inject any arbitrary variable to terraform, you have
to prefix them with `TF_VAR`. Read more about that
[here](https://developer.hashicorp.com/terraform/cli/config/environment-variables#tf_var_name).
</Note>
<Warning>
It is recommended to use Terraform variables to pass your service token dynamically to avoid hard coding it
</Warning>
### 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/cli",
"documentation/getting-started/docker",
"documentation/getting-started/kubernetes"
"documentation/getting-started/kubernetes",
"documentation/getting-started/api"
]
},
{
@@ -250,9 +251,9 @@
{
"group": "Examples",
"pages": [
"api-reference/overview/encryption-modes/overview",
"api-reference/overview/encryption-modes/es-mode",
"api-reference/overview/encryption-modes/e2ee-mode"
"api-reference/overview/examples/note",
"api-reference/overview/examples/e2ee-disabled",
"api-reference/overview/examples/e2ee-enabled"
]
},
"api-reference/overview/blind-indices"

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

View File

@@ -17,6 +17,12 @@
"image": "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",
"slug": "react",

View File

@@ -4,7 +4,11 @@ import { useRouter } from 'next/router';
import { faArrowRight, faCheck, faXmark } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
// 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 ListBox from '@app/components/basic/Listbox';
@@ -27,6 +31,7 @@ interface Integration {
targetEnvironment: string;
workspace: string;
integrationAuth: string;
secretPath: string;
}
interface IntegrationApp {
@@ -55,7 +60,6 @@ const IntegrationTile = ({
environments = [],
handleDeleteIntegration
}: Props) => {
const [integrationEnvironment, setIntegrationEnvironment] = useState<Props['environments'][0]>(
environments.find(({ slug }) => slug === integration?.environment) || {
name: '',
@@ -74,16 +78,16 @@ const IntegrationTile = ({
});
setApps(tempApps);
if (integration?.app) {
setIntegrationApp(integration.app);
} else if (integration?.path && integration?.region) {
setIntegrationApp(`${integration.path} (${integration.region})`);
} else if (tempApps.length > 0) {
setIntegrationApp(tempApps[0].name)
} else {
setIntegrationApp('');
}
setIntegrationApp(tempApps[0].name);
} else {
setIntegrationApp('');
}
switch (integration.integration) {
case 'vercel':
@@ -212,12 +216,15 @@ const IntegrationTile = ({
return <div />;
};
if (!integrationApp && integration.integration !== "checkly") return <div />;
const isSelected = integration.integration === 'hashicorp-vault' ? `${integration.app} - path: ${integration.path}` : integrationApp;
if (!integrationApp && integration.integration !== 'checkly') return <div />;
const isSelected =
integration.integration === 'hashicorp-vault'
? `${integration.app} - path: ${integration.path}`
: integrationApp;
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>
<p className="mb-2 text-xs font-semibold text-gray-400">ENVIRONMENT</p>
@@ -235,33 +242,46 @@ const IntegrationTile = ({
isFull
/>
</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">
<FontAwesomeIcon icon={faArrowRight} className="mx-4 mt-8 text-gray-400" />
</div>
<div className="mr-2">
<p className="text-gray-400 text-xs font-semibold mb-2">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">
<p className="mb-2 text-xs font-semibold text-gray-400">INTEGRATION</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)} */}
{integrationSlugNameMapping[integration.integration]}
</div>
</div>
<div className="mr-2">
<div className="mb-2 text-xs font-semibold text-gray-400">APP</div>
{integrationApp ? <div title={integrationApp}>
<ListBox
data={!integration.isActive ? apps.map((app) => app.name) : null}
isSelected={isSelected}
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>}
{integrationApp ? (
<div title={integrationApp}>
<ListBox
data={!integration.isActive ? apps.map((app) => app.name) : null}
isSelected={isSelected}
onChange={(app) => {
setIntegrationApp(app);
}}
/>
</div>
) : (
<div className="h-10 w-52 animate-pulse rounded-md bg-mineshaft-600 px-4 py-2 font-bold">
-
</div>
)}
</div>
{renderIntegrationSpecificParams(integration)}
</div>
<div className="flex items-end cursor-default">
<div className="flex cursor-default items-end">
{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" />
<div className="font-semibold text-gray-300">In Sync</div>
</div>

View File

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

View File

@@ -76,7 +76,7 @@ export default function InitialLoginStep({
{t('login.continue-with-google')}
</Button>
</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">
<Input
value={email}
@@ -89,7 +89,7 @@ export default function InitialLoginStep({
/>
</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">
<Input
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>
</Link>
</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>
}

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -44,6 +44,7 @@ interface Integration {
integration: string;
targetEnvironment: string;
workspace: string;
secretPath:string;
integrationAuth: string;
}
@@ -441,7 +442,15 @@ export default function Integrations() {
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} />
</div>

View File

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

View File

@@ -56,6 +56,7 @@ export default function AWSSecretManagerCreateIntegrationPage() {
const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? '');
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
const [secretPath, setSecretPath] = useState('/');
const [selectedAWSRegion, setSelectedAWSRegion] = useState('');
const [targetSecretName, setTargetSecretName] = useState('');
const [targetSecretNameErrorText, setTargetSecretNameErrorText] = useState('');
@@ -100,7 +101,8 @@ export default function AWSSecretManagerCreateIntegrationPage() {
targetServiceId: null,
owner: null,
path: null,
region: selectedAWSRegion
region: selectedAWSRegion,
secretPath
});
setIsLoading(false);
@@ -132,6 +134,13 @@ export default function AWSSecretManagerCreateIntegrationPage() {
))}
</Select>
</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">
<Select
value={selectedAWSRegion}

View File

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

View File

@@ -2,7 +2,15 @@ import { useEffect, useState } from 'react';
import { useRouter } from 'next/router';
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 {
useGetIntegrationAuthApps,
useGetIntegrationAuthById
@@ -22,6 +30,8 @@ export default function ChecklyCreateIntegrationPage() {
});
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
const [secretPath, setSecretPath] = useState('/');
const [targetApp, setTargetApp] = useState('');
const [targetAppId, setTargetAppId] = useState('');
@@ -63,7 +73,8 @@ export default function ChecklyCreateIntegrationPage() {
targetServiceId: null,
owner: null,
path: null,
region: null
region: null,
secretPath
});
setIsLoading(false);
@@ -80,8 +91,13 @@ export default function ChecklyCreateIntegrationPage() {
integrationAuthApps &&
targetApp ? (
<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">
<CardTitle className="text-left px-6" subTitle="Choose which environment in Infisical you want to sync with your Checkly account.">Checkly Integration</CardTitle>
<Card className="max-w-lg rounded-md border border-mineshaft-600 p-0">
<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">
<Select
value={selectedSourceEnvironment}
@@ -98,6 +114,13 @@ export default function ChecklyCreateIntegrationPage() {
))}
</Select>
</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">
<Select
value={targetApp}

View File

@@ -2,7 +2,15 @@ import { useEffect, useState } from 'react';
import { useRouter } from 'next/router';
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 {
useGetIntegrationAuthApps,
useGetIntegrationAuthById
@@ -22,6 +30,8 @@ export default function CircleCICreateIntegrationPage() {
});
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
const [secretPath, setSecretPath] = useState('/');
const [targetApp, setTargetApp] = useState('');
const [isLoading, setIsLoading] = useState(false);
@@ -62,7 +72,8 @@ export default function CircleCICreateIntegrationPage() {
targetServiceId: null,
owner: null,
path: null,
region: null
region: null,
secretPath
});
setIsLoading(false);
@@ -97,6 +108,13 @@ export default function CircleCICreateIntegrationPage() {
))}
</Select>
</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">
<Select
value={targetApp}

View File

@@ -2,7 +2,15 @@ import { useEffect, useState } from 'react';
import { useRouter } from 'next/router';
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 {
useGetIntegrationAuthApps,
useGetIntegrationAuthById
@@ -22,6 +30,8 @@ export default function FlyioCreateIntegrationPage() {
});
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
const [secretPath, setSecretPath] = useState('/');
const [targetApp, setTargetApp] = useState('');
const [isLoading, setIsLoading] = useState(false);
@@ -61,7 +71,8 @@ export default function FlyioCreateIntegrationPage() {
targetServiceId: null,
owner: null,
path: null,
region: null
region: null,
secretPath
});
setIsLoading(false);
@@ -96,6 +107,13 @@ export default function FlyioCreateIntegrationPage() {
))}
</Select>
</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">
<Select
value={targetApp}

View File

@@ -2,7 +2,15 @@ import { useEffect, useState } from 'react';
import { useRouter } from 'next/router';
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 {
useGetIntegrationAuthApps,
useGetIntegrationAuthById
@@ -22,6 +30,7 @@ export default function GitHubCreateIntegrationPage() {
});
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
const [secretPath, setSecretPath] = useState('/');
const [targetAppId, setTargetAppId] = useState('');
const [isLoading, setIsLoading] = useState(false);
@@ -66,7 +75,8 @@ export default function GitHubCreateIntegrationPage() {
targetServiceId: null,
owner: targetApp.owner,
path: null,
region: null
region: null,
secretPath
});
setIsLoading(false);
@@ -100,6 +110,13 @@ export default function GitHubCreateIntegrationPage() {
))}
</Select>
</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">
<Select
value={targetAppId}

View File

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

View File

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

View File

@@ -2,7 +2,15 @@ import { useEffect, useState } from 'react';
import { useRouter } from 'next/router';
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 {
useGetIntegrationAuthApps,
useGetIntegrationAuthById
@@ -23,6 +31,7 @@ export default function HerokuCreateIntegrationPage() {
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
const [targetApp, setTargetApp] = useState('');
const [secretPath, setSecretPath] = useState('/');
const [isLoading, setIsLoading] = useState(false);
@@ -60,7 +69,8 @@ export default function HerokuCreateIntegrationPage() {
targetServiceId: null,
owner: null,
path: null,
region: null
region: null,
secretPath
});
setIsLoading(false);
@@ -94,6 +104,13 @@ export default function HerokuCreateIntegrationPage() {
))}
</Select>
</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">
<Select
value={targetApp}

View File

@@ -2,7 +2,15 @@ import { useEffect, useState } from 'react';
import { useRouter } from 'next/router';
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 {
useGetIntegrationAuthApps,
useGetIntegrationAuthById
@@ -31,7 +39,7 @@ export default function NetlifyCreateIntegrationPage() {
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
const [targetApp, setTargetApp] = useState('');
const [targetEnvironment, setTargetEnvironment] = useState('');
const [secretPath, setSecretPath] = useState('/');
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
@@ -71,7 +79,8 @@ export default function NetlifyCreateIntegrationPage() {
targetServiceId: null,
owner: null,
path: null,
region: null
region: null,
secretPath
});
setIsLoading(false);
@@ -106,6 +115,13 @@ export default function NetlifyCreateIntegrationPage() {
))}
</Select>
</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">
<Select
value={targetApp}

View File

@@ -2,7 +2,15 @@ import { useEffect, useState } from 'react';
import { useRouter } from 'next/router';
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 {
useGetIntegrationAuthApps,
useGetIntegrationAuthById,
@@ -20,6 +28,7 @@ export default function RailwayCreateIntegrationPage() {
const [targetServiceId, setTargetServiceId] = useState('');
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
const [secretPath, setSecretPath] = useState('/');
const [isLoading, setIsLoading] = useState(false);
const { integrationAuthId } = queryString.parse(router.asPath.split('?')[1]);
@@ -99,7 +108,8 @@ export default function RailwayCreateIntegrationPage() {
targetServiceId: targetService ? targetService.serviceId : null,
owner: null,
path: null,
region: null
region: null,
secretPath
});
setIsLoading(false);
@@ -134,6 +144,13 @@ export default function RailwayCreateIntegrationPage() {
))}
</Select>
</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">
<Select
value={targetAppId}

View File

@@ -2,7 +2,15 @@ import { useEffect, useState } from 'react';
import { useRouter } from 'next/router';
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 {
useGetIntegrationAuthApps,
useGetIntegrationAuthById
@@ -23,7 +31,7 @@ export default function RenderCreateIntegrationPage() {
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
const [targetApp, setTargetApp] = useState('');
const [secretPath, setSecretPath] = useState('/');
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
@@ -62,7 +70,8 @@ export default function RenderCreateIntegrationPage() {
targetServiceId: null,
owner: null,
path: null,
region: null
region: null,
secretPath
});
setIsLoading(false);
@@ -97,6 +106,13 @@ export default function RenderCreateIntegrationPage() {
))}
</Select>
</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">
<Select
value={targetApp}

View File

@@ -2,7 +2,15 @@ import { useEffect, useState } from 'react';
import { useRouter } from 'next/router';
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 {
useGetIntegrationAuthApps,
useGetIntegrationAuthById
@@ -22,6 +30,7 @@ export default function SupabaseCreateIntegrationPage() {
});
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
const [secretPath, setSecretPath] = useState('/');
const [targetApp, setTargetApp] = useState('');
const [isLoading, setIsLoading] = useState(false);
@@ -62,7 +71,8 @@ export default function SupabaseCreateIntegrationPage() {
targetServiceId: null,
owner: null,
path: null,
region: null
region: null,
secretPath
});
setIsLoading(false);
@@ -97,6 +107,13 @@ export default function SupabaseCreateIntegrationPage() {
))}
</Select>
</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">
<Select
value={targetApp}

View File

@@ -2,7 +2,15 @@ import { useEffect, useState } from 'react';
import { useRouter } from 'next/router';
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 {
useGetIntegrationAuthApps,
useGetIntegrationAuthById
@@ -23,7 +31,7 @@ export default function TravisCICreateIntegrationPage() {
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
const [targetApp, setTargetApp] = useState('');
const [secretPath, setSecretPath] = useState('/');
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
@@ -62,7 +70,8 @@ export default function TravisCICreateIntegrationPage() {
targetServiceId: null,
owner: null,
path: null,
region: null
region: null,
secretPath
});
setIsLoading(false);
@@ -97,6 +106,13 @@ export default function TravisCICreateIntegrationPage() {
))}
</Select>
</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">
<Select
value={targetApp}

View File

@@ -2,7 +2,15 @@ import { useEffect, useState } from 'react';
import { useRouter } from 'next/router';
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 {
useGetIntegrationAuthApps,
useGetIntegrationAuthById,
@@ -21,6 +29,7 @@ export default function VercelCreateIntegrationPage() {
const router = useRouter();
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
const [secretPath, setSecretPath] = useState('/');
const [targetAppId, setTargetAppId] = useState('');
const [targetEnvironment, setTargetEnvironment] = useState('');
const [targetBranch, setTargetBranch] = useState('');
@@ -84,7 +93,8 @@ export default function VercelCreateIntegrationPage() {
targetServiceId: null,
owner: null,
path,
region: null
region: null,
secretPath
});
setIsLoading(false);
@@ -119,6 +129,13 @@ export default function VercelCreateIntegrationPage() {
))}
</Select>
</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">
<Select
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">
Enter your backup key
</p>
<div className="mt-4 flex flex-row items-center justify-center md:mx-2 md:pb-4">
<p className="flex w-max max-w-md justify-center text-sm text-gray-400">
You can find it in your emrgency kit. You had to download the enrgency kit during signup.
<div className="flex flex-row items-center justify-center md:pb-4 mt-4 md:mx-2">
<p className="text-sm flex justify-center text-gray-400 w-max max-w-md">
You can find it in your emergency kit. You had to download the emergency kit during signup.
</p>
</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">

View File

@@ -4,20 +4,20 @@
@layer utilities {
.flex-0 {
flex:0;
flex: 0;
}
.flex-2 {
flex-grow: 2;
}
.flex-3 {
flex-grow: 3;
flex-grow: 3;
}
}
@layer components {
.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 */
@@ -29,13 +29,45 @@
@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;
}
.secret-table tr {
@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';

View File

@@ -1,35 +1,31 @@
import { useEffect, useMemo, useState } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
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 { yupResolver } from '@hookform/resolvers/yup';
import NavHeader from '@app/components/navigation/NavHeader';
import { Button, Input, TableContainer, Tooltip } from '@app/components/v2';
import { useWorkspace } from '@app/context';
import {
useGetProjectFoldersBatch,
useGetProjectSecretsByKey,
useGetUserWsEnvironments,
useGetUserWsKey
} from '@app/hooks/api';
import { WorkspaceEnv } from '@app/hooks/api/types';
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 router = useRouter();
const [selectedEnv, setSelectedEnv] = useState<WorkspaceEnv | null>(null);
const { currentWorkspace, isLoading } = useWorkspace();
const workspaceId = currentWorkspace?._id as string;
const { data: latestFileKey } = useGetUserWsKey(workspaceId);
const [searchFilter, setSearchFilter] = useState('');
const secretPath = router.query?.secretPath as string;
useEffect(() => {
if (!isLoading && !workspaceId && router.isReady) {
@@ -38,14 +34,7 @@ export const DashboardEnvOverview = ({ onEnvChange }: { onEnvChange: any }) => {
}, [isLoading, workspaceId, router.isReady]);
const { data: wsEnv, isLoading: isEnvListLoading } = useGetUserWsEnvironments({
workspaceId,
onSuccess: (data) => {
// get an env with one of the access available
const env = data.find(({ isReadDenied }) => !isReadDenied);
if (env) {
setSelectedEnv(env);
}
}
workspaceId
});
const userAvailableEnvs = wsEnv?.filter(({ isReadDenied }) => !isReadDenied);
@@ -54,17 +43,32 @@ export const DashboardEnvOverview = ({ onEnvChange }: { onEnvChange: any }) => {
workspaceId,
env: userAvailableEnvs?.map((env) => env.slug) ?? [],
decryptFileKey: latestFileKey!,
isPaused: false
isPaused: false,
secretPath
});
const method = useForm<FormData>({
// why any: well yup inferred ts expects other keys to defined as undefined
defaultValues: secrets as any,
values: secrets as any,
mode: 'onBlur',
resolver: yupResolver(schema)
const folders = useGetProjectFoldersBatch({
folders:
userAvailableEnvs?.map((env) => ({
environment: env.slug,
workspaceId
})) ?? [],
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(() => {
// first get all sec in the env then subtract with total to get missing ones
const secPerEnvMissing: Record<string, number> = Object.fromEntries(
@@ -81,7 +85,43 @@ export const DashboardEnvOverview = ({ onEnvChange }: { onEnvChange: any }) => {
return secPerEnvMissing;
}, [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) {
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
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 (
<div className="container mx-auto max-w-full px-6 text-mineshaft-50 dark:[color-scheme:dark]">
<FormProvider {...method}>
<form autoComplete="off">
{/* breadcrumb row */}
<div className="relative right-5">
<NavHeader pageName={t('dashboard.title')} isProjectRelated />
<div className="relative right-5">
<NavHeader pageName={t('dashboard.title')} isProjectRelated />
</div>
<div className="mt-6">
<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 className="mt-6">
<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"
{(secretPath || '')
.split('/')
.filter(Boolean)
.map((path, index, arr) => (
<div
key={`secret-path-${index + 1}`}
className={`breadcrumb relative z-20 ${
index + 1 === arr.length ? 'cursor-default' : 'cursor-pointer'
} 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
</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>
{path}
</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 className="text-sm font-medium ">Secret</div>
</div>
</div>
{numSecretsMissingPerEnv &&
userAvailableEnvs?.map((env) => {
return (
<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 className="w-80">
<Input
className="h-[2.3rem] bg-mineshaft-800 placeholder-mineshaft-50 duration-200 focus:bg-mineshaft-700/80"
placeholder="Search by secret/folder name..."
value={searchFilter}
onChange={(e) => setSearchFilter(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
/>
</div>
</div>
<div className="overflow-y-auto">
<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">
<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 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 className="text-sm font-medium ">Secret</div>
</div>
</div>
{numSecretsMissingPerEnv &&
userAvailableEnvs?.map((env) => {
return (
<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
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>
)}
{/* In future, we should add an option to add environments here
<div className="flex items-start justify-center h-full ml-10">
<Button
leftIcon={<FontAwesomeIcon icon={faPlus}/>}
onClick={() => prepend(DEFAULT_SECRET_VALUE, { shouldFocus: false })}
variant="outline_bg"
colorSchema="primary"
isFullWidth
className="h-10"
>
Add Environment
</Button>
</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
className={`${
isDashboardEmpty ? '' : ''
} 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`}
>
{!isDashboardEmpty && (
<TableContainer className="border-none">
<table className="secret-table relative w-full bg-mineshaft-900">
<tbody className="max-h-screen overflow-y-auto">
{Object.keys(foldersGroupedByEnv || {}).map((folderName, index) => (
<FolderComparisonRow
key={`${folderName}-${index + 1}`}
folderName={folderName}
userAvailableEnvs={userAvailableEnvs}
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 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 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>
</form>
</FormProvider>
<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={() => onExploreEnv(env.slug)}
variant="outline_bg"
colorSchema="primary"
isFullWidth
className="h-10"
>
Explore {env.name}
</Button>
</div>
);
})}
</div>
</div>
</div>
);
};

View File

@@ -1,11 +1,10 @@
/* eslint-disable react/jsx-no-useless-fragment */
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 { twMerge } from 'tailwind-merge';
type Props = {
index: number;
secrets: any[] | undefined;
// permission and external state's that decided to hide or show
isReadOnly?: boolean;
@@ -30,7 +29,7 @@ const DashboardInput = ({
if (val === undefined)
return (
<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>
);
if (val?.length === 0)
@@ -110,7 +109,6 @@ const DashboardInput = ({
};
export const EnvComparisonRow = ({
index,
secrets,
isSecretValueHidden,
isReadOnly,
@@ -126,7 +124,9 @@ export const EnvComparisonRow = ({
return (
<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">
<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 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">

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,
CreateUpdateEnvFormData,
CreateWsTag,
E2EESection,
EnvironmentSection,
ProjectIndexSecretsSection,
ProjectNameChangeSection,
ServiceTokenSection,
E2EESection
} from './components';
ServiceTokenSection} from './components';
export const ProjectSettingsPage = () => {
const { t } = useTranslation();
@@ -90,8 +89,8 @@ export const ProjectSettingsPage = () => {
// get user subscription
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) => {
try {