mirror of
https://github.com/Infisical/infisical.git
synced 2025-03-28 15:29:21 +00:00
Compare commits
37 Commits
infisical/
...
infisical/
Author | SHA1 | Date | |
---|---|---|---|
b98e276767 | |||
149c58fa3e | |||
62d79b82f8 | |||
7f7e63236b | |||
965a5cc113 | |||
af31549309 | |||
072e5013fc | |||
43f2cf8dc3 | |||
0aca308bbd | |||
15fc12627a | |||
c77ebd4d0e | |||
ccaf9a9ffc | |||
391e37d49e | |||
7088b3c9d8 | |||
ccf0877b81 | |||
0aa9390ece | |||
e47934a08a | |||
04b7383bbe | |||
930b1e8d0c | |||
82a026a426 | |||
92647341a9 | |||
776cecc3ef | |||
a4fb2378bb | |||
9742fdc770 | |||
786778fef6 | |||
3f946180dd | |||
b1b32a34c9 | |||
3d70333f9c | |||
a6cf7107b9 | |||
d590dd5db8 | |||
f4404f66b8 | |||
9a62496d5c | |||
e24c1f38e0 | |||
3ca9b7d6bf | |||
37d2d580f4 | |||
41dd2fda8a | |||
22ca4f2e92 |
4
.github/values.yaml
vendored
4
.github/values.yaml
vendored
@ -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
|
||||
|
118
.github/workflows/build-docker-image-to-prod.yml
vendored
Normal file
118
.github/workflows/build-docker-image-to-prod.yml
vendored
Normal 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 }}
|
@ -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
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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',
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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({
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -170,7 +170,7 @@ export const updateSubscriptionOrgQuantity = async ({
|
||||
);
|
||||
}
|
||||
|
||||
await EELicenseService.refreshOrganizationPlan(organizationId);
|
||||
await EELicenseService.refreshPlan(organizationId);
|
||||
|
||||
return stripeSubscription;
|
||||
};
|
@ -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) => {
|
||||
|
@ -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
|
||||
|
@ -41,7 +41,7 @@ export const createWorkspace = async ({
|
||||
workspaceId: workspace._id
|
||||
});
|
||||
|
||||
await EELicenseService.refreshOrganizationPlan(organizationId);
|
||||
await EELicenseService.refreshPlan(organizationId);
|
||||
|
||||
return workspace;
|
||||
};
|
||||
|
@ -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
|
||||
);
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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"`
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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() {
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
||||

|
||||

|
||||
</Accordion>
|
||||
<Accordion title="Infisical Token">
|
||||

|
||||
</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.
|
||||
|
||||

|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||

|
||||

|
||||
</Tab>
|
||||
</Tabs>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
||||
|
176
docs/api-reference/overview/examples/e2ee-disabled.mdx
Normal file
176
docs/api-reference/overview/examples/e2ee-disabled.mdx
Normal 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>
|
@ -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';
|
54
docs/api-reference/overview/examples/note.mdx
Normal file
54
docs/api-reference/overview/examples/note.mdx
Normal 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>
|
@ -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>
|
@ -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>
|
@ -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>
|
59
docs/documentation/getting-started/api.mdx
Normal file
59
docs/documentation/getting-started/api.mdx
Normal 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)
|
@ -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
|
||||
|
@ -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).
|
||||
|
@ -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"
|
||||
|
@ -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:
|
||||
|
@ -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>
|
||||
}
|
||||
|
64
frontend/src/components/utilities/intercom/intercom.ts
Normal file
64
frontend/src/components/utilities/intercom/intercom.ts
Normal file
@ -0,0 +1,64 @@
|
||||
/* eslint-disable prefer-template */
|
||||
/* eslint-disable prefer-rest-params */
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions */
|
||||
/* eslint-disable no-nested-ternary */
|
||||
/* eslint-disable no-unexpected-multiline */
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
/* eslint-disable vars-on-top */
|
||||
/* eslint-disable no-var */
|
||||
/* eslint-disable func-names */
|
||||
// @ts-nocheck
|
||||
|
||||
import { INTERCOM_ID as APP_ID } from '@app/components/utilities/config';
|
||||
|
||||
// Loads Intercom with the snippet
|
||||
// This must be run before boot, it initializes window.Intercom
|
||||
|
||||
// prettier-ignore
|
||||
export const load = () => {
|
||||
(function(){
|
||||
var w=window;
|
||||
var ic=w.Intercom;
|
||||
|
||||
if(typeof ic==="function"){
|
||||
ic('reattach_activator');
|
||||
ic('update',w.intercomSettings);
|
||||
} else {
|
||||
var d=document;
|
||||
var i=function() {
|
||||
i.c(arguments);
|
||||
};
|
||||
i.q=[];
|
||||
i.c=function(args) {
|
||||
i.q.push(args);
|
||||
};
|
||||
w.Intercom=i;
|
||||
var l=function() {
|
||||
var s=d.createElement('script');
|
||||
s.type='text/javascript';
|
||||
s.async=true;
|
||||
s.src='https://widget.intercom.io/widget/' + APP_ID;
|
||||
var x=d.getElementsByTagName('script')[0];
|
||||
x.parentNode.insertBefore(s, x);
|
||||
};
|
||||
if (document.readyState==='complete') {
|
||||
l();
|
||||
} else if (w.attachEvent) {
|
||||
w.attachEvent('onload',l);
|
||||
} else {
|
||||
w.addEventListener('load',l,false);
|
||||
}
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
// Initializes Intercom
|
||||
export const boot = (options = {}) => {
|
||||
window &&
|
||||
window.Intercom &&
|
||||
window.Intercom("boot", { app_id: APP_ID, ...options });
|
||||
};
|
||||
|
||||
export const update = () => {
|
||||
window && window.Intercom && window.Intercom("update");
|
||||
};
|
@ -0,0 +1,37 @@
|
||||
import { useEffect } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import {
|
||||
boot as bootIntercom,
|
||||
load as loadIntercom,
|
||||
update as updateIntercom,
|
||||
} from "./intercom";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export const IntercomProvider = ({ children }: { children: any }) => {
|
||||
const router = useRouter();
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
loadIntercom();
|
||||
bootIntercom();
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const handleRouteChange = (url: string) => {
|
||||
if (typeof window !== "undefined") {
|
||||
updateIntercom();
|
||||
}
|
||||
};
|
||||
|
||||
router.events.on("routeChangeStart", handleRouteChange);
|
||||
|
||||
// If the component is unmounted, unsubscribe
|
||||
// from the event with the `off` method:
|
||||
return () => {
|
||||
router.events.off("routeChangeStart", handleRouteChange);
|
||||
};
|
||||
}, [router.events]);
|
||||
|
||||
return children;
|
||||
};
|
@ -1 +1,7 @@
|
||||
export { useCreateFolder, useDeleteFolder, useGetProjectFolders, useUpdateFolder } from './queries';
|
||||
export {
|
||||
useCreateFolder,
|
||||
useDeleteFolder,
|
||||
useGetProjectFolders,
|
||||
useGetProjectFoldersBatch,
|
||||
useUpdateFolder
|
||||
} from './queries';
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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 = () => {
|
||||
|
@ -96,6 +96,7 @@ export type GetProjectSecretsDTO = {
|
||||
env: string | string[];
|
||||
decryptFileKey: UserWsKeyPair;
|
||||
folderId?: string;
|
||||
secretPath?: string;
|
||||
isPaused?: boolean;
|
||||
onSuccess?: (data: DecryptedSecret[]) => void;
|
||||
};
|
||||
|
@ -12,5 +12,5 @@ export type SubscriptionPlan = {
|
||||
tier: number;
|
||||
workspaceLimit: number;
|
||||
workspacesUsed: number;
|
||||
envLimit: number;
|
||||
environmentLimit: number;
|
||||
};
|
||||
|
@ -21,7 +21,6 @@ import * as yup from 'yup';
|
||||
import { useNotificationContext } from '@app/components/context/Notifications/NotificationProvider';
|
||||
import onboardingCheck from '@app/components/utilities/checks/OnboardingCheck';
|
||||
import { tempLocalStorage } from '@app/components/utilities/checks/tempLocalStorage';
|
||||
import { INTERCOM_ID } from '@app/components/utilities/config';
|
||||
import { encryptAssymmetric } from '@app/components/utilities/cryptography/crypto';
|
||||
import {
|
||||
Button,
|
||||
@ -90,46 +89,6 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
// Intercom code snippet
|
||||
(function() {
|
||||
var w=window;var ic=w.Intercom;
|
||||
if(typeof ic==="function") {
|
||||
ic('reattach_activator');
|
||||
ic('update',w.intercomSettings);
|
||||
} else {
|
||||
var d=document;
|
||||
var i=function() {
|
||||
// eslint-disable-next-line prefer-rest-params
|
||||
i.c(arguments);
|
||||
};
|
||||
i.q=[];
|
||||
i.c=function(args) {
|
||||
i.q.push(args);
|
||||
};
|
||||
w.Intercom=i;
|
||||
var l=function() {
|
||||
var s=d.createElement('script');
|
||||
s.type='text/javascript';
|
||||
s.async=true;
|
||||
s.src=`https://widget.intercom.io/widget/${INTERCOM_ID}`;
|
||||
var x=d.getElementsByTagName('script')[0];
|
||||
x.parentNode.insertBefore(s,x);};
|
||||
if(w.attachEvent) {
|
||||
w.attachEvent('onload',l);
|
||||
} else {
|
||||
w.addEventListener('load',l,false);
|
||||
}
|
||||
}
|
||||
}
|
||||
)();
|
||||
|
||||
window.Intercom('boot', {
|
||||
app_id: {INTERCOM_ID},
|
||||
email: user.email || 'undefined'
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const handleRouteChange = () => {
|
||||
(window).Intercom('update');
|
||||
|
@ -11,6 +11,7 @@ import { config } from '@fortawesome/fontawesome-svg-core';
|
||||
import { QueryClientProvider } from '@tanstack/react-query';
|
||||
|
||||
import NotificationProvider from '@app/components/context/Notifications/NotificationProvider';
|
||||
import { IntercomProvider } from '@app/components/utilities/intercom/intercomProvider';
|
||||
import Telemetry from '@app/components/utilities/telemetry/Telemetry';
|
||||
import { TooltipProvider } from '@app/components/v2';
|
||||
import { publicPaths } from '@app/const';
|
||||
@ -81,9 +82,11 @@ const App = ({ Component, pageProps, ...appProps }: NextAppProp): JSX.Element =>
|
||||
<SubscriptionProvider>
|
||||
<UserProvider>
|
||||
<NotificationProvider>
|
||||
<AppLayout>
|
||||
<Component {...pageProps} />
|
||||
</AppLayout>
|
||||
<IntercomProvider>
|
||||
<AppLayout>
|
||||
<Component {...pageProps} />
|
||||
</AppLayout>
|
||||
</IntercomProvider>
|
||||
</NotificationProvider>
|
||||
</UserProvider>
|
||||
</SubscriptionProvider>
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
|
@ -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">
|
||||
|
@ -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';
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
);
|
@ -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 {
|
||||
|
Reference in New Issue
Block a user