Compare commits

...

73 Commits

Author SHA1 Message Date
Daniel Hougaard
fd66cafe10 Fix: Add height and remove extra spacing 2024-03-11 11:43:04 +01:00
Daniel Hougaard
822d0692db Merge pull request #1549 from rhythmbhiwani/fix-secret-page-with-no-env
Redirecting to overview page if environment doesn't exists in secrets main page
2024-03-11 11:32:45 +01:00
Rhythm Bhiwani
e527d99654 Remove console.log 2024-03-11 15:42:18 +05:30
Daniel Hougaard
628c641580 Merge pull request #1550 from Infisical/daniel/cli-hotfix
Fix: CLI Service Tokens
2024-03-11 10:04:32 +01:00
Daniel Hougaard
40ccab6576 Fix: Failing to get service token 2024-03-11 09:05:56 +01:00
Daniel Hougaard
9cc3e58561 Fix: Failing to get service token 2024-03-11 09:05:54 +01:00
Daniel Hougaard
1f3fded404 Fix: Failing to get service token 2024-03-11 09:05:52 +01:00
Daniel Hougaard
74b5e8cbeb Fix: Failing to get service token 2024-03-11 09:05:47 +01:00
Daniel Hougaard
522a03c2ad Fix: Helper function for getting service token 2024-03-11 09:05:33 +01:00
Rhythm Bhiwani
56bf82e4f6 Redirecting to overview page if environment doesn't exists in secrets main page 2024-03-11 06:52:37 +05:30
BlackMagiq
f1b7653a52 Merge pull request #1419 from Infisical/integration-update-heroku
Integration update heroku
2024-03-09 19:15:00 -08:00
Maidul Islam
0cb6d052e0 Update values.yaml 2024-03-09 20:31:11 -05:00
Maidul Islam
ceb135fc94 Merge pull request #1508 from Infisical/snyk-upgrade-ba8f3acf185100a451cbbadcbe68f789
[Snyk] Upgrade posthog-js from 1.105.4 to 1.105.6
2024-03-09 11:05:45 -05:00
Maidul Islam
b75289f074 Merge branch 'main' into snyk-upgrade-ba8f3acf185100a451cbbadcbe68f789 2024-03-09 11:05:36 -05:00
Maidul Islam
de86705e64 Merge pull request #1515 from rhythmbhiwani/feature-rename-secret-accross-envs
Feature: Rename secret from overview page, accross all environments
2024-03-09 11:02:22 -05:00
Akhil Mohan
6ca56143d9 Merge pull request #1545 from rhythmbhiwani/docs-typo-fixed-cli
Fixed typo in `secrets get` docs
2024-03-09 17:32:45 +05:30
Rhythm Bhiwani
ef0e652557 Fixed typo in secrets get docs 2024-03-09 15:23:25 +05:30
Tuan Dang
89e109e404 Iron out naming / text, update docs for Heroku integration 2024-03-08 18:12:52 -08:00
Maidul Islam
48062d9680 Merge pull request #1542 from akhilmhdh/fix/create-folder-cli
feat(server): added back delete by folder name in api
2024-03-08 17:46:14 -05:00
BlackMagiq
d11fda3be5 Merge pull request #1544 from Infisical/railway-integration
Update Railway integration get services query, make services optional
2024-03-08 14:25:14 -08:00
Maidul Islam
0df5f845fb Update docker-swarm-with-agent.mdx 2024-03-08 17:07:11 -05:00
Tuan Dang
ca59488b62 Update Railway integration get services query, make services optional 2024-03-08 11:46:51 -08:00
Maidul Islam
3a05ae4b27 Merge pull request #1543 from Infisical/docker-swarm-docs
docs: docker swarm with infisical agent
2024-03-08 14:42:56 -05:00
Maidul Islam
dd009182e8 docs: docker swarm with infisical agent 2024-03-08 14:42:02 -05:00
Tuan Dang
8ac7a29893 Draft refactor core secrets fn into reusable factories 2024-03-08 09:06:03 -08:00
Maidul Islam
8a17cd3f5d Merge pull request #1532 from rhythmbhiwani/get-only-value-from-cli
Feature to get only value of specific secret in `secrets get` command
2024-03-08 10:54:10 -05:00
Maidul Islam
99fe43f459 rename --value to --raw-value + polish docs 2024-03-08 10:53:11 -05:00
Akhil Mohan
2e3b10ccfc feat(server): added back delete by folder name in api 2024-03-08 17:45:14 +05:30
Maidul Islam
79196b0081 Update secret-reference.mdx 2024-03-08 00:19:10 -05:00
Maidul Islam
b76ff28414 Update secret-reference.mdx 2024-03-08 00:16:48 -05:00
Maidul Islam
2894cf791a Merge pull request #1538 from Infisical/daniel/agent-template-func
Feat: Agent secret referencing support
2024-03-08 00:04:29 -05:00
Daniel Hougaard
c040b0ca9a Fix: Include Workspace ID in request when expanding secrets with MI's 2024-03-08 06:02:44 +01:00
Daniel Hougaard
15f60aa7dd Fix: Add WorkspaceID field to env variable struct 2024-03-08 06:02:29 +01:00
Daniel Hougaard
6f68d304ea Fix: Get Service Token from env vars earlier 2024-03-08 06:02:15 +01:00
Tuan Dang
0b98feea50 Make sync behavior apply on first sync only, finish MVP create/update bidirectional sync for Heroku 2024-03-07 18:06:56 -08:00
Tuan Dang
43d40d7475 MVP preliminary idea for initial sync behavior 2024-03-07 15:25:53 -08:00
Maidul Islam
309a106f13 patch create folder on cli 2024-03-07 17:02:35 -05:00
Maidul Islam
74d73590a1 add docker manifest to go releaser 2024-03-07 15:38:01 -05:00
Maidul Islam
b42b5614c9 add buildx to workflow 2024-03-07 15:21:16 -05:00
Maidul Islam
72b89cb989 try buildx to support multi arch 2024-03-07 15:14:14 -05:00
Daniel Hougaard
36d8b22598 Feat: Agent secret referencing support 2024-03-07 07:03:16 +01:00
Daniel Hougaard
201dcd971c Feat: Agent secret referencing support 2024-03-07 06:49:57 +01:00
Daniel Hougaard
ab90745312 Feat: Agent secret referencing support (Auth input) 2024-03-07 06:49:28 +01:00
Daniel Hougaard
622106045e Feat: Agent secret referencing support (update ExpandSecrets input) 2024-03-07 06:48:52 +01:00
Daniel Hougaard
e64302b789 Feat: Agent secret referencing support (update ExpandSecrets input) 2024-03-07 06:48:48 +01:00
Daniel Hougaard
901a7fc294 Feat: Agent secret referencing support (update ExpandSecrets input) 2024-03-07 06:48:43 +01:00
Daniel Hougaard
359694dd47 Chore: Cleanup 2024-03-07 06:48:08 +01:00
Daniel Hougaard
57489a7578 Merge pull request #1537 from Infisical/daniel/copy-project-slug
Feat: Copy project slug button
2024-03-07 02:27:24 +01:00
Daniel Hougaard
a4205a8662 Cleanup 🧼 2024-03-07 02:22:52 +01:00
Daniel Hougaard
dbf177d667 Feat: Add copy project slug button 2024-03-07 02:20:01 +01:00
Daniel Hougaard
f078aec54c Feat: Add copy project slug button 2024-03-07 02:19:54 +01:00
Daniel Hougaard
5dfe62e306 Feat: Copy project slug button 2024-03-07 02:01:31 +01:00
Daniel Hougaard
b89925c61c Feat: Copy project slug button 2024-03-07 02:01:23 +01:00
Maidul Islam
6d0bea6d5f Update .goreleaser.yaml to support arm 2024-03-06 18:42:27 -05:00
Daniel Hougaard
10a40c8ab2 Merge pull request #1535 from Infisical/daniel/better-upgrade-errors
Fix: Edge case causing project upgrade to fail
2024-03-07 00:07:05 +01:00
Maidul Islam
b910ceacfc create secret on overview typo 2024-03-06 17:50:31 -05:00
Maidul Islam
cb66386e13 Merge pull request #1536 from Infisical/daniel/fix-project-memberships
Fix: Remove project keys & memberships when organization membership is deleted
2024-03-06 17:38:49 -05:00
Daniel Hougaard
ae53f03f71 Fix: Remove project memberships & project keys when org membership is deleted (DAL) 2024-03-06 23:15:13 +01:00
Daniel Hougaard
7ae024724d Fix: Remove project memberships & project keys when org membership is deleted (Service) 2024-03-06 23:15:02 +01:00
Daniel Hougaard
0b2bc1d345 Fix: Remove project memberships & project keys when org membership is deleted 2024-03-06 23:14:52 +01:00
Daniel Hougaard
da5eca3e68 Fix: Seeding not working 2024-03-06 23:13:22 +01:00
Maidul Islam
d140e4f3c9 update bulk add message 2024-03-06 14:12:21 -05:00
Rhythm Bhiwani
8e25631fb0 Updated the docs 2024-03-05 16:14:20 +05:30
Rhythm Bhiwani
0912903e0d Added --value flag to secrets get command to return only value 2024-03-05 16:04:21 +05:30
Rhythm Bhiwani
d8860e1ce3 Disabled submit button when renaming all keys if key name is empty 2024-03-03 02:49:35 +05:30
Rhythm Bhiwani
3fa529dcb0 Added error message if name is empty 2024-03-02 09:30:03 +05:30
Rhythm Bhiwani
b6f3cf512e spacing made consistent 2024-03-02 06:57:36 +05:30
Rhythm Bhiwani
4dbee7df06 Added notification on success and failure renaming secret 2024-03-02 06:45:52 +05:30
Rhythm Bhiwani
323c412f5e Added Option to Rename Secrets from overview page in all environments 2024-03-02 06:41:32 +05:30
snyk-bot
c2fe6eb90c fix: upgrade posthog-js from 1.105.4 to 1.105.6
Snyk has created this PR to upgrade posthog-js from 1.105.4 to 1.105.6.

See this package in npm:
https://www.npmjs.com/package/posthog-js

See this project in Snyk:
https://app.snyk.io/org/maidul98/project/53d4ecb6-6cc1-4918-aa73-bf9cae4ffd13?utm_source=github&utm_medium=referral&page=upgrade-pr
2024-03-01 09:12:34 +00:00
Vladyslav Matsiiako
b1d049c677 added ability to hide environments in the overview screen 2024-02-17 21:49:17 -08:00
Vladyslav Matsiiako
9012012503 added basic heroku pipeline integration 2024-02-17 21:04:26 -08:00
Vladyslav Matsiiako
a8678c14e8 updated heroku integration style 2024-02-16 22:58:56 -08:00
66 changed files with 2887 additions and 558 deletions

2
.github/values.yaml vendored
View File

@@ -27,7 +27,7 @@ infisical:
deploymentAnnotations:
secrets.infisical.com/auto-reload: "true"
kubeSecretRef: "infisical-gamma-secrets"
kubeSecretRef: "managed-secret"
ingress:
## @param ingress.enabled Enable ingress

View File

@@ -23,6 +23,8 @@ jobs:
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: 🔧 Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- run: git fetch --force --tags
- run: echo "Ref name ${{github.ref_name}}"
- uses: actions/setup-go@v3

View File

@@ -190,10 +190,34 @@ dockers:
- dockerfile: docker/alpine
goos: linux
goarch: amd64
use: buildx
ids:
- all-other-builds
image_templates:
- "infisical/cli:{{ .Version }}"
- "infisical/cli:{{ .Major }}.{{ .Minor }}"
- "infisical/cli:{{ .Major }}"
- "infisical/cli:latest"
- "infisical/cli:{{ .Major }}.{{ .Minor }}.{{ .Patch }}-amd64"
- "infisical/cli:latest-amd64"
build_flag_templates:
- "--pull"
- "--platform=linux/amd64"
- dockerfile: docker/alpine
goos: linux
goarch: amd64
use: buildx
ids:
- all-other-builds
image_templates:
- "infisical/cli:{{ .Major }}.{{ .Minor }}.{{ .Patch }}-arm64"
- "infisical/cli:latest-arm64"
build_flag_templates:
- "--pull"
- "--platform=linux/arm64"
docker_manifests:
- name_template: "infisical/cli:{{ .Major }}.{{ .Minor }}.{{ .Patch }}"
image_templates:
- "infisical/cli:{{ .Major }}.{{ .Minor }}.{{ .Patch }}-amd64"
- "infisical/cli:{{ .Major }}.{{ .Minor }}.{{ .Patch }}-arm64"
- name_template: "infisical/cli:latest"
image_templates:
- "infisical/cli:latest-amd64"
- "infisical/cli:latest-arm64"

View File

@@ -0,0 +1,15 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
await knex.schema.alterTable(TableName.Integration, (t) => {
t.datetime("lastUsed");
});
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.alterTable(TableName.Integration, (t) => {
t.dropColumn("lastUsed");
});
}

View File

@@ -27,7 +27,8 @@ export const IntegrationsSchema = z.object({
envId: z.string().uuid(),
secretPath: z.string().default("/"),
createdAt: z.date(),
updatedAt: z.date()
updatedAt: z.date(),
lastUsed: z.date().nullable().optional()
});
export type TIntegrations = z.infer<typeof IntegrationsSchema>;

View File

@@ -9,7 +9,12 @@ export async function seed(knex: Knex): Promise<void> {
await knex(TableName.Users).del();
await knex(TableName.UserEncryptionKey).del();
await knex(TableName.SuperAdmin).del();
await knex(TableName.SuperAdmin).insert([{ initialized: true, allowSignUp: true }]);
await knex(TableName.SuperAdmin).insert([
// eslint-disable-next-line
// @ts-ignore
{ id: "00000000-0000-0000-0000-000000000000", initialized: true, allowSignUp: true }
]);
// Inserts seed entries
const [user] = await knex(TableName.Users)
.insert([

View File

@@ -12,9 +12,11 @@ import { groupBy, pick, unique } from "@app/lib/fn";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { ActorType } from "@app/services/auth/auth-type";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TSecretDALFactory } from "@app/services/secret/secret-dal";
import { TSecretQueueFactory } from "@app/services/secret/secret-queue";
import { TSecretServiceFactory } from "@app/services/secret/secret-service";
import { TSecretVersionDALFactory } from "@app/services/secret/secret-version-dal";
import { TSecretVersionTagDALFactory } from "@app/services/secret/secret-version-tag-dal";
import { TSecretBlindIndexDALFactory } from "@app/services/secret-blind-index/secret-blind-index-dal";
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
import { TSecretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal";
@@ -44,10 +46,12 @@ type TSecretApprovalRequestServiceFactoryDep = {
secretApprovalRequestSecretDAL: TSecretApprovalRequestSecretDALFactory;
secretApprovalRequestReviewerDAL: TSecretApprovalRequestReviewerDALFactory;
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath" | "findById" | "findSecretPathByFolderIds">;
secretTagDAL: Pick<TSecretTagDALFactory, "findManyTagsById">;
secretDAL: TSecretDALFactory;
secretTagDAL: Pick<TSecretTagDALFactory, "findManyTagsById" | "saveTagsToSecret" | "deleteTagsManySecret">;
secretBlindIndexDAL: Pick<TSecretBlindIndexDALFactory, "findOne">;
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
secretVersionDAL: Pick<TSecretVersionDALFactory, "findLatestVersionMany">;
secretVersionDAL: Pick<TSecretVersionDALFactory, "findLatestVersionMany" | "insertMany">;
secretVersionTagDAL: Pick<TSecretVersionTagDALFactory, "insertMany">;
projectDAL: Pick<TProjectDALFactory, "checkProjectUpgradeStatus">;
secretService: Pick<
TSecretServiceFactory,
@@ -64,8 +68,10 @@ export type TSecretApprovalRequestServiceFactory = ReturnType<typeof secretAppro
export const secretApprovalRequestServiceFactory = ({
secretApprovalRequestDAL,
secretDAL,
folderDAL,
secretTagDAL,
secretVersionTagDAL,
secretApprovalRequestReviewerDAL,
secretApprovalRequestSecretDAL,
secretBlindIndexDAL,
@@ -335,7 +341,11 @@ export const secretApprovalRequestServiceFactory = ({
tags: el?.tags.map(({ id }) => id),
version: 1,
type: SecretType.Shared
}))
})),
secretDAL,
secretVersionDAL,
secretTagDAL,
secretVersionTagDAL
})
: [];
const updatedSecrets = secretUpdationCommits.length
@@ -367,7 +377,11 @@ export const secretApprovalRequestServiceFactory = ({
"secretBlindIndex"
])
}
}))
})),
secretDAL,
secretVersionDAL,
secretTagDAL,
secretVersionTagDAL
})
: [];
const deletedSecret = secretDeletionCommits.length
@@ -455,7 +469,8 @@ export const secretApprovalRequestServiceFactory = ({
inputSecrets: createdSecrets,
folderId,
isNew: true,
blindIndexCfg
blindIndexCfg,
secretDAL
});
commits.push(
@@ -482,7 +497,8 @@ export const secretApprovalRequestServiceFactory = ({
inputSecrets: updatedSecrets,
folderId,
isNew: false,
blindIndexCfg
blindIndexCfg,
secretDAL
});
// now find any secret that needs to update its name
@@ -492,7 +508,8 @@ export const secretApprovalRequestServiceFactory = ({
inputSecrets: nameUpdatedSecrets,
folderId,
isNew: true,
blindIndexCfg
blindIndexCfg,
secretDAL
});
const secsGroupedByBlindIndex = groupBy(secretsToBeUpdated, (el) => el.secretBlindIndex as string);
@@ -531,7 +548,8 @@ export const secretApprovalRequestServiceFactory = ({
inputSecrets: deletedSecrets,
folderId,
isNew: false,
blindIndexCfg
blindIndexCfg,
secretDAL
});
const secretsGroupedByBlindIndex = groupBy(secrets, (i) => {
if (!i.secretBlindIndex) throw new BadRequestError({ message: "Missing secret blind index" });

View File

@@ -263,6 +263,8 @@ export const registerRoutes = async (
incidentContactDAL,
tokenService,
projectDAL,
projectMembershipDAL,
projectKeyDAL,
smtpService,
userDAL,
orgBotDAL
@@ -419,7 +421,12 @@ export const registerRoutes = async (
orgDAL,
projectMembershipDAL,
smtpService,
projectDAL
projectDAL,
projectBotDAL,
secretVersionDAL,
secretBlindIndexDAL,
secretTagDAL,
secretVersionTagDAL
});
const secretBlindIndexService = secretBlindIndexServiceFactory({
permissionService,
@@ -443,6 +450,7 @@ export const registerRoutes = async (
const sarService = secretApprovalRequestServiceFactory({
permissionService,
folderDAL,
secretDAL,
secretTagDAL,
secretApprovalRequestSecretDAL: sarSecretDAL,
secretApprovalRequestReviewerDAL: sarReviewerDAL,
@@ -452,6 +460,7 @@ export const registerRoutes = async (
secretApprovalRequestDAL,
secretService,
snapshotService,
secretVersionTagDAL,
secretQueueService
});
const secretRotationQueue = secretRotationQueueFactory({

View File

@@ -513,6 +513,37 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
}
});
server.route({
url: "/:integrationAuthId/heroku/pipelines",
method: "GET",
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
params: z.object({
integrationAuthId: z.string().trim()
}),
response: {
200: z.object({
pipelines: z
.object({
app: z.object({ appId: z.string() }),
stage: z.string(),
pipeline: z.object({ name: z.string(), pipelineId: z.string() })
})
.array()
})
}
},
handler: async (req) => {
const pipelines = await server.services.integrationAuth.getHerokuPipelines({
actorId: req.permission.id,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
id: req.params.integrationAuthId
});
return { pipelines };
}
});
server.route({
url: "/:integrationAuthId/railway/environments",
method: "GET",

View File

@@ -32,6 +32,7 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
.object({
secretPrefix: z.string().optional(),
secretSuffix: z.string().optional(),
initialSyncBehavior: z.string().optional(),
secretGCPLabel: z
.object({
labelName: z.string(),

View File

@@ -120,7 +120,7 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
});
server.route({
url: "/:folderId",
url: "/:folderIdOrName",
method: "DELETE",
schema: {
description: "Delete a folder",
@@ -131,7 +131,7 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
}
],
params: z.object({
folderId: z.string()
folderIdOrName: z.string()
}),
body: z.object({
workspaceId: z.string().trim(),
@@ -155,7 +155,7 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
actorOrgId: req.permission.orgId,
...req.body,
projectId: req.body.workspaceId,
id: req.params.folderId,
idOrName: req.params.folderIdOrName,
path
});
await server.services.auditLog.createAuditLog({

View File

@@ -109,7 +109,7 @@ const getAppsGCPSecretManager = async ({ accessToken }: { accessToken: string })
*/
const getAppsHeroku = async ({ accessToken }: { accessToken: string }) => {
const res = (
await request.get<{ name: string }[]>(`${IntegrationUrls.HEROKU_API_URL}/apps`, {
await request.get<{ name: string; id: string }[]>(`${IntegrationUrls.HEROKU_API_URL}/apps`, {
headers: {
Accept: "application/vnd.heroku+json; version=3",
Authorization: `Bearer ${accessToken}`
@@ -118,7 +118,8 @@ const getAppsHeroku = async ({ accessToken }: { accessToken: string }) => {
).data;
const apps = res.map((a) => ({
name: a.name
name: a.name,
appId: a.id
}));
return apps;

View File

@@ -20,9 +20,11 @@ import {
TDeleteIntegrationAuthsDTO,
TGetIntegrationAuthDTO,
TGetIntegrationAuthTeamCityBuildConfigDTO,
THerokuPipelineCoupling,
TIntegrationAuthAppsDTO,
TIntegrationAuthBitbucketWorkspaceDTO,
TIntegrationAuthChecklyGroupsDTO,
TIntegrationAuthHerokuPipelinesDTO,
TIntegrationAuthNorthflankSecretGroupDTO,
TIntegrationAuthQoveryEnvironmentsDTO,
TIntegrationAuthQoveryOrgsDTO,
@@ -576,6 +578,38 @@ export const integrationAuthServiceFactory = ({
return [];
};
const getHerokuPipelines = async ({ id, actor, actorId, actorOrgId }: TIntegrationAuthHerokuPipelinesDTO) => {
const integrationAuth = await integrationAuthDAL.findById(id);
if (!integrationAuth) throw new BadRequestError({ message: "Failed to find integration" });
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
integrationAuth.projectId,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
const botKey = await projectBotService.getBotKey(integrationAuth.projectId);
const { accessToken } = await getIntegrationAccessToken(integrationAuth, botKey);
const { data } = await request.get<THerokuPipelineCoupling[]>(
`${IntegrationUrls.HEROKU_API_URL}/pipeline-couplings`,
{
headers: {
Accept: "application/vnd.heroku+json; version=3",
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json"
}
}
);
return data.map(({ app: { id: appId }, stage, pipeline: { id: pipelineId, name } }) => ({
app: { appId },
stage,
pipeline: { pipelineId, name }
}));
};
const getRailwayEnvironments = async ({ id, actor, actorId, actorOrgId, appId }: TIntegrationAuthRailwayEnvDTO) => {
const integrationAuth = await integrationAuthDAL.findById(id);
if (!integrationAuth) throw new BadRequestError({ message: "Failed to find integration" });
@@ -649,33 +683,21 @@ export const integrationAuthServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
const botKey = await projectBotService.getBotKey(integrationAuth.projectId);
const { accessToken } = await getIntegrationAccessToken(integrationAuth, botKey);
if (appId) {
if (appId && appId !== "") {
const query = `
query project($id: String!) {
project(id: $id) {
createdAt
deletedAt
id
description
expiredAt
isPublic
isTempProject
isUpdatable
name
prDeploys
teamId
updatedAt
upstreamUrl
services {
edges {
node {
id
name
}
}
}
query project($id: String!) {
project(id: $id) {
services {
edges {
node {
id
name
}
}
}
}
}
}
`;
const variables = {
@@ -711,6 +733,7 @@ export const integrationAuthServiceFactory = ({
);
return edges.map(({ node: { name, id: serviceId } }) => ({ name, serviceId }));
}
return [];
};
@@ -915,6 +938,7 @@ export const integrationAuthServiceFactory = ({
getQoveryApps,
getQoveryEnvs,
getQoveryJobs,
getHerokuPipelines,
getQoveryOrgs,
getQoveryProjects,
getQoveryContainers,

View File

@@ -62,6 +62,10 @@ export type TIntegrationAuthQoveryScopesDTO = {
environmentId: string;
} & Omit<TProjectPermission, "projectId">;
export type TIntegrationAuthHerokuPipelinesDTO = {
id: string;
} & Omit<TProjectPermission, "projectId">;
export type TIntegrationAuthRailwayEnvDTO = {
id: string;
appId: string;
@@ -129,6 +133,12 @@ export type TNorthflankSecretGroup = {
projectId: string;
};
export type THerokuPipelineCoupling = {
app: { id: string };
stage: string;
pipeline: { id: string; name: string };
};
export type TTeamCityBuildConfig = {
id: string;
name: string;

View File

@@ -37,6 +37,12 @@ export enum IntegrationType {
OAUTH2 = "oauth2"
}
export enum IntegrationInitialSyncBehavior {
OVERWRITE_TARGET = "overwrite-target",
PREFER_TARGET = "prefer-target",
PREFER_SOURCE = "prefer-source"
}
export enum IntegrationUrls {
// integration oauth endpoints
GCP_TOKEN_URL = "https://oauth2.googleapis.com/token",

View File

@@ -20,11 +20,13 @@ import sodium from "libsodium-wrappers";
import isEqual from "lodash.isequal";
import { z } from "zod";
import { TIntegrationAuths, TIntegrations } from "@app/db/schemas";
import { SecretType, TIntegrationAuths, TIntegrations, TSecrets } from "@app/db/schemas";
import { request } from "@app/lib/config/request";
import { BadRequestError } from "@app/lib/errors";
import { TCreateManySecretsRawFn, TUpdateManySecretsRawFn } from "@app/services/secret/secret-types";
import { Integrations, IntegrationUrls } from "./integration-list";
import { TIntegrationDALFactory } from "../integration/integration-dal";
import { IntegrationInitialSyncBehavior, Integrations, IntegrationUrls } from "./integration-list";
const getSecretKeyValuePair = (secrets: Record<string, { value: string | null; comment?: string } | null>) =>
Object.keys(secrets).reduce<Record<string, string | null | undefined>>((prev, key) => {
@@ -582,11 +584,25 @@ const syncSecretsAWSSecretManager = async ({
* Sync/push [secrets] to Heroku app named [integration.app]
*/
const syncSecretsHeroku = async ({
createManySecretsRawFn,
updateManySecretsRawFn,
integrationDAL,
integration,
secrets,
accessToken
}: {
integration: TIntegrations;
createManySecretsRawFn: (params: TCreateManySecretsRawFn) => Promise<Array<TSecrets & { _id: string }>>;
updateManySecretsRawFn: (params: TUpdateManySecretsRawFn) => Promise<Array<TSecrets & { _id: string }>>;
integrationDAL: Pick<TIntegrationDALFactory, "updateById">;
integration: TIntegrations & {
projectId: string;
environment: {
id: string;
name: string;
slug: string;
};
secretPath: string;
};
secrets: Record<string, { value: string; comment?: string } | null>;
accessToken: string;
}) => {
@@ -600,12 +616,74 @@ const syncSecretsHeroku = async ({
})
).data;
const secretsToAdd: { [key: string]: string } = {};
const secretsToUpdate: { [key: string]: string } = {};
const metadata = z.record(z.any()).parse(integration.metadata);
Object.keys(herokuSecrets).forEach((key) => {
if (!(key in secrets)) {
secrets[key] = null;
}
if (!integration.lastUsed) {
// first time using integration
// -> apply initial sync behavior
switch (metadata.initialSyncBehavior) {
case IntegrationInitialSyncBehavior.OVERWRITE_TARGET: {
if (!(key in secrets)) secrets[key] = null;
break;
}
case IntegrationInitialSyncBehavior.PREFER_TARGET: {
if (!(key in secrets)) {
secretsToAdd[key] = herokuSecrets[key];
} else if (secrets[key]?.value !== herokuSecrets[key]) {
secretsToUpdate[key] = herokuSecrets[key];
}
secrets[key] = {
value: herokuSecrets[key]
};
break;
}
case IntegrationInitialSyncBehavior.PREFER_SOURCE: {
if (!(key in secrets)) {
secrets[key] = herokuSecrets[key];
secretsToAdd[key] = herokuSecrets[key];
}
break;
}
default: {
if (!(key in secrets)) secrets[key] = null;
break;
}
}
} else if (!(key in secrets)) secrets[key] = null;
});
if (Object.keys(secretsToAdd).length) {
await createManySecretsRawFn({
projectId: integration.projectId,
environment: integration.environment.slug,
path: integration.secretPath,
secrets: Object.keys(secretsToAdd).map((key) => ({
secretName: key,
secretValue: secretsToAdd[key],
type: SecretType.Shared,
secretComment: ""
}))
});
}
if (Object.keys(secretsToUpdate).length) {
await updateManySecretsRawFn({
projectId: integration.projectId,
environment: integration.environment.slug,
path: integration.secretPath,
secrets: Object.keys(secretsToUpdate).map((key) => ({
secretName: key,
secretValue: secretsToUpdate[key],
type: SecretType.Shared,
secretComment: ""
}))
});
}
await request.patch(
`${IntegrationUrls.HEROKU_API_URL}/apps/${integration.app}/config-vars`,
getSecretKeyValuePair(secrets),
@@ -617,6 +695,10 @@ const syncSecretsHeroku = async ({
}
}
);
await integrationDAL.updateById(integration.id, {
lastUsed: new Date()
});
};
/**
@@ -1204,21 +1286,21 @@ const syncSecretsRailway = async ({
}
`;
const input = {
projectId: integration.appId,
environmentId: integration.targetEnvironmentId,
...(integration.targetServiceId ? { serviceId: integration.targetServiceId } : {}),
replace: true,
variables: getSecretKeyValuePair(secrets)
const variables = {
input: {
projectId: integration.appId,
environmentId: integration.targetEnvironmentId,
...(integration.targetServiceId ? { serviceId: integration.targetServiceId } : {}),
replace: true,
variables: getSecretKeyValuePair(secrets)
}
};
await request.post(
IntegrationUrls.RAILWAY_API_URL,
{
query,
variables: {
input
}
variables
},
{
headers: {
@@ -2930,8 +3012,14 @@ const syncSecretsHasuraCloud = async ({
/**
* Sync/push [secrets] to [app] in integration named [integration]
*
* Do this in terms of DAL
*
*/
export const syncIntegrationSecrets = async ({
createManySecretsRawFn,
updateManySecretsRawFn,
integrationDAL,
integration,
integrationAuth,
secrets,
@@ -2939,7 +3027,18 @@ export const syncIntegrationSecrets = async ({
accessToken,
appendices
}: {
integration: TIntegrations;
createManySecretsRawFn: (params: TCreateManySecretsRawFn) => Promise<Array<TSecrets & { _id: string }>>;
updateManySecretsRawFn: (params: TUpdateManySecretsRawFn) => Promise<Array<TSecrets & { _id: string }>>;
integrationDAL: Pick<TIntegrationDALFactory, "updateById">;
integration: TIntegrations & {
projectId: string;
environment: {
id: string;
name: string;
slug: string;
};
secretPath: string;
};
integrationAuth: TIntegrationAuths;
secrets: Record<string, { value: string; comment?: string }>;
accessId: string | null;
@@ -2979,6 +3078,9 @@ export const syncIntegrationSecrets = async ({
break;
case Integrations.HEROKU:
await syncSecretsHeroku({
createManySecretsRawFn,
updateManySecretsRawFn,
integrationDAL,
integration,
secrets,
accessToken

View File

@@ -22,6 +22,8 @@ import { ActorType, AuthMethod, AuthTokenType } from "../auth/auth-type";
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
import { TokenType } from "../auth-token/auth-token-types";
import { TProjectDALFactory } from "../project/project-dal";
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
import { TUserDALFactory } from "../user/user-dal";
import { TIncidentContactsDALFactory } from "./incident-contacts-dal";
@@ -44,6 +46,8 @@ type TOrgServiceFactoryDep = {
orgRoleDAL: TOrgRoleDALFactory;
userDAL: TUserDALFactory;
projectDAL: TProjectDALFactory;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findProjectMembershipsByUserId" | "delete">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete">;
incidentContactDAL: TIncidentContactsDALFactory;
samlConfigDAL: Pick<TSamlConfigDALFactory, "findOne" | "findEnforceableSamlCfg">;
smtpService: TSmtpService;
@@ -65,6 +69,8 @@ export const orgServiceFactory = ({
permissionService,
smtpService,
projectDAL,
projectMembershipDAL,
projectKeyDAL,
tokenService,
orgBotDAL,
licenseService,
@@ -503,10 +509,50 @@ export const orgServiceFactory = ({
const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Member);
const membership = await orgDAL.deleteMembershipById(membershipId, orgId);
const deletedMembership = await orgDAL.transaction(async (tx) => {
const orgMembership = await orgDAL.deleteMembershipById(membershipId, orgId, tx);
await licenseService.updateSubscriptionOrgMemberCount(orgId);
return membership;
if (!orgMembership.userId) {
await licenseService.updateSubscriptionOrgMemberCount(orgId);
return orgMembership;
}
// Get all the project memberships of the user in the organization
const projectMemberships = await projectMembershipDAL.findProjectMembershipsByUserId(orgId, orgMembership.userId);
// Delete all the project memberships of the user in the organization
await projectMembershipDAL.delete(
{
$in: {
id: projectMemberships.map((membership) => membership.id)
}
},
tx
);
// Get all the project keys of the user in the organization
const projectKeys = await projectKeyDAL.find({
$in: {
projectId: projectMemberships.map((membership) => membership.projectId)
},
receiverId: orgMembership.userId
});
// Delete all the project keys of the user in the organization
await projectKeyDAL.delete(
{
$in: {
id: projectKeys.map((key) => key.id)
}
},
tx
);
await licenseService.updateSubscriptionOrgMemberCount(orgId);
return orgMembership;
});
return deletedMembership;
};
/*

View File

@@ -0,0 +1,36 @@
import { SecretKeyEncoding } from "@app/db/schemas";
import { decryptAsymmetric, infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
import { BadRequestError } from "@app/lib/errors";
import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
import { TGetPrivateKeyDTO } from "./project-bot-types";
export const getBotPrivateKey = ({ bot }: TGetPrivateKeyDTO) =>
infisicalSymmetricDecrypt({
keyEncoding: bot.keyEncoding as SecretKeyEncoding,
iv: bot.iv,
tag: bot.tag,
ciphertext: bot.encryptedPrivateKey
});
export const getBotKeyFnFactory = (projectBotDAL: TProjectBotDALFactory) => {
const getBotKeyFn = async (projectId: string) => {
const bot = await projectBotDAL.findOne({ projectId });
if (!bot) throw new BadRequestError({ message: "failed to find bot key" });
if (!bot.isActive) throw new BadRequestError({ message: "Bot is not active" });
if (!bot.encryptedProjectKeyNonce || !bot.encryptedProjectKey)
throw new BadRequestError({ message: "Encryption key missing" });
const botPrivateKey = getBotPrivateKey({ bot });
return decryptAsymmetric({
ciphertext: bot.encryptedProjectKey,
privateKey: botPrivateKey,
nonce: bot.encryptedProjectKeyNonce,
publicKey: bot.sender.publicKey
});
};
return getBotKeyFn;
};

View File

@@ -1,15 +1,16 @@
import { ForbiddenError } from "@casl/ability";
import { ProjectVersion, SecretKeyEncoding } from "@app/db/schemas";
import { ProjectVersion } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { decryptAsymmetric, generateAsymmetricKeyPair } from "@app/lib/crypto";
import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { generateAsymmetricKeyPair } from "@app/lib/crypto";
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { BadRequestError } from "@app/lib/errors";
import { TProjectDALFactory } from "../project/project-dal";
import { TProjectBotDALFactory } from "./project-bot-dal";
import { TFindBotByProjectIdDTO, TGetPrivateKeyDTO, TSetActiveStateDTO } from "./project-bot-types";
import { getBotKeyFnFactory, getBotPrivateKey } from "./project-bot-fns";
import { TFindBotByProjectIdDTO, TSetActiveStateDTO } from "./project-bot-types";
type TProjectBotServiceFactoryDep = {
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
@@ -24,29 +25,10 @@ export const projectBotServiceFactory = ({
projectDAL,
permissionService
}: TProjectBotServiceFactoryDep) => {
const getBotPrivateKey = ({ bot }: TGetPrivateKeyDTO) =>
infisicalSymmetricDecrypt({
keyEncoding: bot.keyEncoding as SecretKeyEncoding,
iv: bot.iv,
tag: bot.tag,
ciphertext: bot.encryptedPrivateKey
});
const getBotKeyFn = getBotKeyFnFactory(projectBotDAL);
const getBotKey = async (projectId: string) => {
const bot = await projectBotDAL.findOne({ projectId });
if (!bot) throw new BadRequestError({ message: "failed to find bot key" });
if (!bot.isActive) throw new BadRequestError({ message: "Bot is not active" });
if (!bot.encryptedProjectKeyNonce || !bot.encryptedProjectKey)
throw new BadRequestError({ message: "Encryption key missing" });
const botPrivateKey = getBotPrivateKey({ bot });
return decryptAsymmetric({
ciphertext: bot.encryptedProjectKey,
privateKey: botPrivateKey,
nonce: bot.encryptedProjectKeyNonce,
publicKey: bot.sender.publicKey
});
return getBotKeyFn(projectId);
};
const findBotByProjectId = async ({

View File

@@ -82,5 +82,25 @@ export const projectMembershipDALFactory = (db: TDbClient) => {
}
};
return { ...projectMemberOrm, findAllProjectMembers, findProjectGhostUser, findMembershipsByEmail };
const findProjectMembershipsByUserId = async (orgId: string, userId: string) => {
try {
const memberships = await db(TableName.ProjectMembership)
.where({ userId })
.join(TableName.Project, `${TableName.ProjectMembership}.projectId`, `${TableName.Project}.id`)
.where({ [`${TableName.Project}.orgId` as "orgId"]: orgId })
.select(selectAllTableCols(TableName.ProjectMembership));
return memberships;
} catch (error) {
throw new DatabaseError({ error, name: "Find project memberships by user id" });
}
};
return {
...projectMemberOrm,
findAllProjectMembers,
findProjectGhostUser,
findMembershipsByEmail,
findProjectMembershipsByUserId
};
};

View File

@@ -1,6 +1,6 @@
import { ForbiddenError, subject } from "@casl/ability";
import path from "path";
import { v4 as uuidv4 } from "uuid";
import { v4 as uuidv4, validate as uuidValidate } from "uuid";
import { TSecretFoldersInsert } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
@@ -164,7 +164,7 @@ export const secretFolderServiceFactory = ({
actorOrgId,
environment,
path: secretPath,
id
idOrName
}: TDeleteFolderDTO) => {
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(
@@ -179,7 +179,10 @@ export const secretFolderServiceFactory = ({
const parentFolder = await folderDAL.findBySecretPath(projectId, environment, secretPath, tx);
if (!parentFolder) throw new BadRequestError({ message: "Secret path not found" });
const [doc] = await folderDAL.delete({ envId: env.id, id, parentId: parentFolder.id }, tx);
const [doc] = await folderDAL.delete(
{ envId: env.id, [uuidValidate(idOrName) ? "id" : "name"]: idOrName, parentId: parentFolder.id },
tx
);
if (!doc) throw new BadRequestError({ message: "Folder not found", name: "Delete folder" });
return doc;
});

View File

@@ -16,7 +16,7 @@ export type TUpdateFolderDTO = {
export type TDeleteFolderDTO = {
environment: string;
path: string;
id: string;
idOrName: string;
} & TProjectPermission;
export type TGetFolderDTO = {

View File

@@ -1,12 +1,35 @@
/* eslint-disable no-await-in-loop */
import path from "path";
import { SecretKeyEncoding, TSecretBlindIndexes, TSecrets } from "@app/db/schemas";
import {
SecretEncryptionAlgo,
SecretKeyEncoding,
SecretType,
TableName,
TSecretBlindIndexes,
TSecrets
} from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env";
import { buildSecretBlindIndexFromName, decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
import {
buildSecretBlindIndexFromName,
decryptSymmetric128BitHexKeyUTF8,
encryptSymmetric128BitHexKeyUTF8
} from "@app/lib/crypto";
import { BadRequestError } from "@app/lib/errors";
import { groupBy, unique } from "@app/lib/fn";
import { getBotKeyFnFactory } from "../project-bot/project-bot-fns";
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TSecretDALFactory } from "./secret-dal";
import {
TCreateManySecretsRawFn,
TCreateManySecretsRawFnFactory,
TFnSecretBlindIndexCheck,
TFnSecretBulkInsert,
TFnSecretBulkUpdate,
TUpdateManySecretsRawFn,
TUpdateManySecretsRawFnFactory
} from "./secret-types";
export const generateSecretBlindIndexBySalt = async (secretName: string, secretBlindIndexDoc: TSecretBlindIndexes) => {
const appCfg = getConfig();
@@ -228,3 +251,399 @@ export const decryptSecretRaw = (secret: TSecrets & { workspace: string; environ
user: secret.userId
};
};
/**
* Checks and handles secrets using a blind index method.
* The function generates mappings between secret names and their blind indexes, validates user IDs for personal secrets, and retrieves secrets from the database based on their blind indexes.
* For new secrets (isNew = true), it ensures they don't already exist in the database.
* For existing secrets, it verifies their presence in the database.
* If discrepancies are found, errors are thrown. The function returns mappings and the fetched secrets.
*/
export const fnSecretBlindIndexCheck = async ({
inputSecrets,
folderId,
isNew,
userId,
blindIndexCfg,
secretDAL
}: TFnSecretBlindIndexCheck) => {
const blindIndex2KeyName: Record<string, string> = {}; // used at audit log point
const keyName2BlindIndex = await Promise.all(
inputSecrets.map(({ secretName }) => generateSecretBlindIndexBySalt(secretName, blindIndexCfg))
).then((blindIndexes) =>
blindIndexes.reduce<Record<string, string>>((prev, curr, i) => {
// eslint-disable-next-line
prev[inputSecrets[i].secretName] = curr;
blindIndex2KeyName[curr] = inputSecrets[i].secretName;
return prev;
}, {})
);
if (inputSecrets.some(({ type }) => type === SecretType.Personal) && !userId) {
throw new BadRequestError({ message: "Missing user id for personal secret" });
}
const secrets = await secretDAL.findByBlindIndexes(
folderId,
inputSecrets.map(({ secretName, type }) => ({
blindIndex: keyName2BlindIndex[secretName],
type: type || SecretType.Shared
})),
userId
);
if (isNew) {
if (secrets.length) throw new BadRequestError({ message: "Secret already exist" });
} else {
const secretKeysInDB = unique(secrets, (el) => el.secretBlindIndex as string).map(
(el) => blindIndex2KeyName[el.secretBlindIndex as string]
);
const hasUnknownSecretsProvided = secretKeysInDB.length !== inputSecrets.length;
if (hasUnknownSecretsProvided) {
const keysMissingInDB = Object.keys(keyName2BlindIndex).filter((key) => !secretKeysInDB.includes(key));
throw new BadRequestError({
message: `Secret not found: blind index ${keysMissingInDB.join(",")}`
});
}
}
return { blindIndex2KeyName, keyName2BlindIndex, secrets };
};
// these functions are special functions shared by a couple of resources
// used by secret approval, rotation or anywhere in which secret needs to modified
export const fnSecretBulkInsert = async ({
// TODO: Pick types here
folderId,
inputSecrets,
secretDAL,
secretVersionDAL,
secretTagDAL,
secretVersionTagDAL,
tx
}: TFnSecretBulkInsert) => {
const newSecrets = await secretDAL.insertMany(
inputSecrets.map(({ tags, ...el }) => ({ ...el, folderId })),
tx
);
const newSecretGroupByBlindIndex = groupBy(newSecrets, (item) => item.secretBlindIndex as string);
const newSecretTags = inputSecrets.flatMap(({ tags: secretTags = [], secretBlindIndex }) =>
secretTags.map((tag) => ({
[`${TableName.SecretTag}Id` as const]: tag,
[`${TableName.Secret}Id` as const]: newSecretGroupByBlindIndex[secretBlindIndex as string][0].id
}))
);
const secretVersions = await secretVersionDAL.insertMany(
inputSecrets.map(({ tags, ...el }) => ({
...el,
folderId,
secretId: newSecretGroupByBlindIndex[el.secretBlindIndex as string][0].id
})),
tx
);
if (newSecretTags.length) {
const secTags = await secretTagDAL.saveTagsToSecret(newSecretTags, tx);
const secVersionsGroupBySecId = groupBy(secretVersions, (i) => i.secretId);
const newSecretVersionTags = secTags.flatMap(({ secretsId, secret_tagsId }) => ({
[`${TableName.SecretVersion}Id` as const]: secVersionsGroupBySecId[secretsId][0].id,
[`${TableName.SecretTag}Id` as const]: secret_tagsId
}));
await secretVersionTagDAL.insertMany(newSecretVersionTags, tx);
}
return newSecrets.map((secret) => ({ ...secret, _id: secret.id }));
};
export const fnSecretBulkUpdate = async ({
tx,
inputSecrets,
folderId,
projectId,
secretDAL,
secretVersionDAL,
secretTagDAL,
secretVersionTagDAL
}: TFnSecretBulkUpdate) => {
const newSecrets = await secretDAL.bulkUpdate(
inputSecrets.map(({ filter, data: { tags, ...data } }) => ({
filter: { ...filter, folderId },
data
})),
tx
);
const secretVersions = await secretVersionDAL.insertMany(
newSecrets.map(({ id, createdAt, updatedAt, ...el }) => ({
...el,
secretId: id
})),
tx
);
const secsUpdatedTag = inputSecrets.flatMap(({ data: { tags } }, i) =>
tags !== undefined ? { tags, secretId: newSecrets[i].id } : []
);
if (secsUpdatedTag.length) {
await secretTagDAL.deleteTagsManySecret(
projectId,
secsUpdatedTag.map(({ secretId }) => secretId),
tx
);
const newSecretTags = secsUpdatedTag.flatMap(({ tags: secretTags = [], secretId }) =>
secretTags.map((tag) => ({
[`${TableName.SecretTag}Id` as const]: tag,
[`${TableName.Secret}Id` as const]: secretId
}))
);
if (newSecretTags.length) {
const secTags = await secretTagDAL.saveTagsToSecret(newSecretTags, tx);
const secVersionsGroupBySecId = groupBy(secretVersions, (i) => i.secretId);
const newSecretVersionTags = secTags.flatMap(({ secretsId, secret_tagsId }) => ({
[`${TableName.SecretVersion}Id` as const]: secVersionsGroupBySecId[secretsId][0].id,
[`${TableName.SecretTag}Id` as const]: secret_tagsId
}));
await secretVersionTagDAL.insertMany(newSecretVersionTags, tx);
}
}
return newSecrets.map((secret) => ({ ...secret, _id: secret.id }));
};
export const createManySecretsRawFnFactory = ({
projectDAL,
projectBotDAL,
secretDAL,
secretVersionDAL,
secretBlindIndexDAL,
secretTagDAL,
secretVersionTagDAL,
folderDAL
}: TCreateManySecretsRawFnFactory) => {
const getBotKeyFn = getBotKeyFnFactory(projectBotDAL);
const createManySecretsRawFn = async ({
projectId,
environment,
path: secretPath,
secrets,
userId
}: TCreateManySecretsRawFn) => {
const botKey = await getBotKeyFn(projectId);
if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
await projectDAL.checkProjectUpgradeStatus(projectId);
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create secret" });
const folderId = folder.id;
const blindIndexCfg = await secretBlindIndexDAL.findOne({ projectId });
if (!blindIndexCfg) throw new BadRequestError({ message: "Blind index not found", name: "Create secret" });
// insert operation
const { keyName2BlindIndex } = await fnSecretBlindIndexCheck({
inputSecrets: secrets,
folderId,
isNew: true,
blindIndexCfg,
secretDAL
});
const inputSecrets = await Promise.all(
secrets.map(async (secret) => {
const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(secret.secretName, botKey);
const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(secret.secretValue || "", botKey);
const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8(secret.secretComment || "", botKey);
if (secret.type === SecretType.Personal) {
if (!userId) throw new BadRequestError({ message: "Missing user id for personal secret" });
const sharedExist = await secretDAL.findOne({
secretBlindIndex: keyName2BlindIndex[secret.secretName],
folderId,
type: SecretType.Shared
});
if (!sharedExist)
throw new BadRequestError({
message: "Failed to create personal secret override for no corresponding shared secret"
});
}
const tags = secret.tags ? await secretTagDAL.findManyTagsById(projectId, secret.tags) : [];
if ((secret.tags || []).length !== tags.length) throw new BadRequestError({ message: "Tag not found" });
return {
type: secret.type,
userId: secret.type === SecretType.Personal ? userId : null,
secretName: secret.secretName,
secretKeyCiphertext: secretKeyEncrypted.ciphertext,
secretKeyIV: secretKeyEncrypted.iv,
secretKeyTag: secretKeyEncrypted.tag,
secretValueCiphertext: secretValueEncrypted.ciphertext,
secretValueIV: secretValueEncrypted.iv,
secretValueTag: secretValueEncrypted.tag,
secretCommentCiphertext: secretCommentEncrypted.ciphertext,
secretCommentIV: secretCommentEncrypted.iv,
secretCommentTag: secretCommentEncrypted.tag,
skipMultilineEncoding: secret.skipMultilineEncoding,
tags: secret.tags
};
})
);
const newSecrets = await secretDAL.transaction(async (tx) =>
fnSecretBulkInsert({
inputSecrets: inputSecrets.map(({ secretName, ...el }) => ({
...el,
version: 0,
secretBlindIndex: keyName2BlindIndex[secretName],
algorithm: SecretEncryptionAlgo.AES_256_GCM,
keyEncoding: SecretKeyEncoding.UTF8
})),
folderId,
secretDAL,
secretVersionDAL,
secretTagDAL,
secretVersionTagDAL,
tx
})
);
return newSecrets;
};
return createManySecretsRawFn;
};
export const updateManySecretsRawFnFactory = ({
projectDAL,
projectBotDAL,
secretDAL,
secretVersionDAL,
secretBlindIndexDAL,
secretTagDAL,
secretVersionTagDAL,
folderDAL
}: TUpdateManySecretsRawFnFactory) => {
const getBotKeyFn = getBotKeyFnFactory(projectBotDAL);
const updateManySecretsRawFn = async ({
projectId,
environment,
path: secretPath,
secrets, // consider accepting instead ciphertext secrets
userId
}: TUpdateManySecretsRawFn): Promise<Array<TSecrets & { _id: string }>> => {
const botKey = await getBotKeyFn(projectId);
if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
await projectDAL.checkProjectUpgradeStatus(projectId);
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Update secret" });
const folderId = folder.id;
const blindIndexCfg = await secretBlindIndexDAL.findOne({ projectId });
if (!blindIndexCfg) throw new BadRequestError({ message: "Blind index not found", name: "Update secret" });
const { keyName2BlindIndex } = await fnSecretBlindIndexCheck({
inputSecrets: secrets,
folderId,
isNew: false,
blindIndexCfg,
secretDAL,
userId
});
const inputSecrets = await Promise.all(
secrets.map(async (secret) => {
if (secret.newSecretName === "") {
throw new BadRequestError({ message: "New secret name cannot be empty" });
}
const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(secret.secretName, botKey);
const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(secret.secretValue || "", botKey);
const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8(secret.secretComment || "", botKey);
if (secret.type === SecretType.Personal) {
if (!userId) throw new BadRequestError({ message: "Missing user id for personal secret" });
const sharedExist = await secretDAL.findOne({
secretBlindIndex: keyName2BlindIndex[secret.secretName],
folderId,
type: SecretType.Shared
});
if (!sharedExist)
throw new BadRequestError({
message: "Failed to update personal secret override for no corresponding shared secret"
});
if (secret.newSecretName)
throw new BadRequestError({ message: "Personal secret cannot change the key name" });
}
const tags = secret.tags ? await secretTagDAL.findManyTagsById(projectId, secret.tags) : [];
if ((secret.tags || []).length !== tags.length) throw new BadRequestError({ message: "Tag not found" });
return {
type: secret.type,
userId: secret.type === SecretType.Personal ? userId : null,
secretName: secret.secretName,
newSecretName: secret.newSecretName,
secretKeyCiphertext: secretKeyEncrypted.ciphertext,
secretKeyIV: secretKeyEncrypted.iv,
secretKeyTag: secretKeyEncrypted.tag,
secretValueCiphertext: secretValueEncrypted.ciphertext,
secretValueIV: secretValueEncrypted.iv,
secretValueTag: secretValueEncrypted.tag,
secretCommentCiphertext: secretCommentEncrypted.ciphertext,
secretCommentIV: secretCommentEncrypted.iv,
secretCommentTag: secretCommentEncrypted.tag,
skipMultilineEncoding: secret.skipMultilineEncoding,
tags: secret.tags
};
})
);
const tagIds = inputSecrets.flatMap(({ tags = [] }) => tags);
const tags = tagIds.length ? await secretTagDAL.findManyTagsById(projectId, tagIds) : [];
if (tagIds.length !== tags.length) throw new BadRequestError({ message: "Tag not found" });
// now find any secret that needs to update its name
// same process as above
const nameUpdatedSecrets = inputSecrets.filter(({ newSecretName }) => Boolean(newSecretName));
const { keyName2BlindIndex: newKeyName2BlindIndex } = await fnSecretBlindIndexCheck({
inputSecrets: nameUpdatedSecrets,
folderId,
isNew: true,
blindIndexCfg,
secretDAL
});
const updatedSecrets = await secretDAL.transaction(async (tx) =>
fnSecretBulkUpdate({
folderId,
projectId,
tx,
inputSecrets: inputSecrets.map(({ secretName, newSecretName, ...el }) => ({
filter: { secretBlindIndex: keyName2BlindIndex[secretName], type: SecretType.Shared },
data: {
...el,
folderId,
secretBlindIndex:
newSecretName && newKeyName2BlindIndex[newSecretName]
? newKeyName2BlindIndex[newSecretName]
: keyName2BlindIndex[secretName],
algorithm: SecretEncryptionAlgo.AES_256_GCM,
keyEncoding: SecretKeyEncoding.UTF8
}
})),
secretDAL,
secretVersionDAL,
secretTagDAL,
secretVersionTagDAL
})
);
return updatedSecrets;
};
return updateManySecretsRawFn;
};

View File

@@ -6,6 +6,12 @@ import { BadRequestError } from "@app/lib/errors";
import { isSamePath } from "@app/lib/fn";
import { logger } from "@app/lib/logger";
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
import { createManySecretsRawFnFactory, updateManySecretsRawFnFactory } from "@app/services/secret/secret-fns";
import { TSecretVersionDALFactory } from "@app/services/secret/secret-version-dal";
import { TSecretVersionTagDALFactory } from "@app/services/secret/secret-version-tag-dal";
import { TSecretBlindIndexDALFactory } from "@app/services/secret-blind-index/secret-blind-index-dal";
import { TSecretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal";
import { TIntegrationDALFactory } from "../integration/integration-dal";
import { TIntegrationAuthServiceFactory } from "../integration-auth/integration-auth-service";
@@ -29,18 +35,23 @@ export type TSecretQueueFactory = ReturnType<typeof secretQueueFactory>;
type TSecretQueueFactoryDep = {
queueService: TQueueServiceFactory;
integrationDAL: Pick<TIntegrationDALFactory, "findByProjectIdV2">;
integrationDAL: Pick<TIntegrationDALFactory, "findByProjectIdV2" | "updateById">;
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
integrationAuthService: Pick<TIntegrationAuthServiceFactory, "getIntegrationAccessToken">;
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath" | "findByManySecretPath">;
secretDAL: Pick<TSecretDALFactory, "findByFolderId" | "find">;
folderDAL: TSecretFolderDALFactory;
secretDAL: TSecretDALFactory;
secretImportDAL: Pick<TSecretImportDALFactory, "find">;
webhookDAL: Pick<TWebhookDALFactory, "findAllWebhooks" | "transaction" | "update" | "bulkUpdate">;
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
projectDAL: Pick<TProjectDALFactory, "findById">;
projectDAL: TProjectDALFactory;
projectBotDAL: TProjectBotDALFactory;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findAllProjectMembers">;
smtpService: TSmtpService;
orgDAL: Pick<TOrgDALFactory, "findOrgByProjectId">;
secretVersionDAL: TSecretVersionDALFactory;
secretBlindIndexDAL: TSecretBlindIndexDALFactory;
secretTagDAL: TSecretTagDALFactory;
secretVersionTagDAL: TSecretVersionTagDALFactory;
};
export type TGetSecrets = {
@@ -62,8 +73,35 @@ export const secretQueueFactory = ({
orgDAL,
smtpService,
projectDAL,
projectMembershipDAL
projectBotDAL,
projectMembershipDAL,
secretVersionDAL,
secretBlindIndexDAL,
secretTagDAL,
secretVersionTagDAL
}: TSecretQueueFactoryDep) => {
const createManySecretsRawFn = createManySecretsRawFnFactory({
projectDAL,
projectBotDAL,
secretDAL,
secretVersionDAL,
secretBlindIndexDAL,
secretTagDAL,
secretVersionTagDAL,
folderDAL
});
const updateManySecretsRawFn = updateManySecretsRawFnFactory({
projectDAL,
projectBotDAL,
secretDAL,
secretVersionDAL,
secretBlindIndexDAL,
secretTagDAL,
secretVersionTagDAL,
folderDAL
});
const syncIntegrations = async (dto: TGetSecrets) => {
await queueService.queue(QueueName.IntegrationSync, QueueJobs.IntegrationSync, dto, {
attempts: 5,
@@ -307,6 +345,9 @@ export const secretQueueFactory = ({
}
await syncIntegrationSecrets({
createManySecretsRawFn,
updateManySecretsRawFn,
integrationDAL,
integration,
integrationAuth,
secrets: Object.keys(suffixedSecrets).length !== 0 ? suffixedSecrets : secrets,

View File

@@ -1,13 +1,13 @@
import { ForbiddenError, subject } from "@casl/ability";
import { SecretEncryptionAlgo, SecretKeyEncoding, SecretsSchema, SecretType, TableName } from "@app/db/schemas";
import { SecretEncryptionAlgo, SecretKeyEncoding, SecretsSchema, SecretType } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service";
import { getConfig } from "@app/lib/config/env";
import { buildSecretBlindIndexFromName, encryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
import { BadRequestError } from "@app/lib/errors";
import { groupBy, pick, unique } from "@app/lib/fn";
import { groupBy, pick } from "@app/lib/fn";
import { logger } from "@app/lib/logger";
import { ActorType } from "../auth/auth-type";
@@ -19,7 +19,7 @@ import { TSecretImportDALFactory } from "../secret-import/secret-import-dal";
import { fnSecretsFromImports } from "../secret-import/secret-import-fns";
import { TSecretTagDALFactory } from "../secret-tag/secret-tag-dal";
import { TSecretDALFactory } from "./secret-dal";
import { decryptSecretRaw, generateSecretBlindIndexBySalt } from "./secret-fns";
import { decryptSecretRaw, fnSecretBlindIndexCheck, fnSecretBulkInsert, fnSecretBulkUpdate } from "./secret-fns";
import { TSecretQueueFactory } from "./secret-queue";
import {
TCreateBulkSecretDTO,
@@ -28,11 +28,8 @@ import {
TDeleteBulkSecretDTO,
TDeleteSecretDTO,
TDeleteSecretRawDTO,
TFnSecretBlindIndexCheck,
TFnSecretBlindIndexCheckV2,
TFnSecretBulkDelete,
TFnSecretBulkInsert,
TFnSecretBulkUpdate,
TGetASecretDTO,
TGetASecretRawDTO,
TGetSecretsDTO,
@@ -95,85 +92,6 @@ export const secretServiceFactory = ({
return secretBlindIndex;
};
// these functions are special functions shared by a couple of resources
// used by secret approval, rotation or anywhere in which secret needs to modified
const fnSecretBulkInsert = async ({ folderId, inputSecrets, tx }: TFnSecretBulkInsert) => {
const newSecrets = await secretDAL.insertMany(
inputSecrets.map(({ tags, ...el }) => ({ ...el, folderId })),
tx
);
const newSecretGroupByBlindIndex = groupBy(newSecrets, (item) => item.secretBlindIndex as string);
const newSecretTags = inputSecrets.flatMap(({ tags: secretTags = [], secretBlindIndex }) =>
secretTags.map((tag) => ({
[`${TableName.SecretTag}Id` as const]: tag,
[`${TableName.Secret}Id` as const]: newSecretGroupByBlindIndex[secretBlindIndex as string][0].id
}))
);
const secretVersions = await secretVersionDAL.insertMany(
inputSecrets.map(({ tags, ...el }) => ({
...el,
folderId,
secretId: newSecretGroupByBlindIndex[el.secretBlindIndex as string][0].id
})),
tx
);
if (newSecretTags.length) {
const secTags = await secretTagDAL.saveTagsToSecret(newSecretTags, tx);
const secVersionsGroupBySecId = groupBy(secretVersions, (i) => i.secretId);
const newSecretVersionTags = secTags.flatMap(({ secretsId, secret_tagsId }) => ({
[`${TableName.SecretVersion}Id` as const]: secVersionsGroupBySecId[secretsId][0].id,
[`${TableName.SecretTag}Id` as const]: secret_tagsId
}));
await secretVersionTagDAL.insertMany(newSecretVersionTags, tx);
}
return newSecrets.map((secret) => ({ ...secret, _id: secret.id }));
};
const fnSecretBulkUpdate = async ({ tx, inputSecrets, folderId, projectId }: TFnSecretBulkUpdate) => {
const newSecrets = await secretDAL.bulkUpdate(
inputSecrets.map(({ filter, data: { tags, ...data } }) => ({
filter: { ...filter, folderId },
data
})),
tx
);
const secretVersions = await secretVersionDAL.insertMany(
newSecrets.map(({ id, createdAt, updatedAt, ...el }) => ({
...el,
secretId: id
})),
tx
);
const secsUpdatedTag = inputSecrets.flatMap(({ data: { tags } }, i) =>
tags !== undefined ? { tags, secretId: newSecrets[i].id } : []
);
if (secsUpdatedTag.length) {
await secretTagDAL.deleteTagsManySecret(
projectId,
secsUpdatedTag.map(({ secretId }) => secretId),
tx
);
const newSecretTags = secsUpdatedTag.flatMap(({ tags: secretTags = [], secretId }) =>
secretTags.map((tag) => ({
[`${TableName.SecretTag}Id` as const]: tag,
[`${TableName.Secret}Id` as const]: secretId
}))
);
if (newSecretTags.length) {
const secTags = await secretTagDAL.saveTagsToSecret(newSecretTags, tx);
const secVersionsGroupBySecId = groupBy(secretVersions, (i) => i.secretId);
const newSecretVersionTags = secTags.flatMap(({ secretsId, secret_tagsId }) => ({
[`${TableName.SecretVersion}Id` as const]: secVersionsGroupBySecId[secretsId][0].id,
[`${TableName.SecretTag}Id` as const]: secret_tagsId
}));
await secretVersionTagDAL.insertMany(newSecretVersionTags, tx);
}
}
return newSecrets.map((secret) => ({ ...secret, _id: secret.id }));
};
const fnSecretBulkDelete = async ({ folderId, inputSecrets, tx, actorId }: TFnSecretBulkDelete) => {
const deletedSecrets = await secretDAL.deleteMany(
inputSecrets.map(({ type, secretBlindIndex }) => ({
@@ -202,63 +120,6 @@ export const secretServiceFactory = ({
return deletedSecrets;
};
/**
* Checks and handles secrets using a blind index method.
* The function generates mappings between secret names and their blind indexes, validates user IDs for personal secrets, and retrieves secrets from the database based on their blind indexes.
* For new secrets (isNew = true), it ensures they don't already exist in the database.
* For existing secrets, it verifies their presence in the database.
* If discrepancies are found, errors are thrown. The function returns mappings and the fetched secrets.
*/
const fnSecretBlindIndexCheck = async ({
inputSecrets,
folderId,
isNew,
userId,
blindIndexCfg
}: TFnSecretBlindIndexCheck) => {
const blindIndex2KeyName: Record<string, string> = {}; // used at audit log point
const keyName2BlindIndex = await Promise.all(
inputSecrets.map(({ secretName }) => generateSecretBlindIndexBySalt(secretName, blindIndexCfg))
).then((blindIndexes) =>
blindIndexes.reduce<Record<string, string>>((prev, curr, i) => {
// eslint-disable-next-line
prev[inputSecrets[i].secretName] = curr;
blindIndex2KeyName[curr] = inputSecrets[i].secretName;
return prev;
}, {})
);
if (inputSecrets.some(({ type }) => type === SecretType.Personal) && !userId) {
throw new BadRequestError({ message: "Missing user id for personal secret" });
}
const secrets = await secretDAL.findByBlindIndexes(
folderId,
inputSecrets.map(({ secretName, type }) => ({
blindIndex: keyName2BlindIndex[secretName],
type: type || SecretType.Shared
})),
userId
);
if (isNew) {
if (secrets.length) throw new BadRequestError({ message: "Secret already exist" });
} else {
const secretKeysInDB = unique(secrets, (el) => el.secretBlindIndex as string).map(
(el) => blindIndex2KeyName[el.secretBlindIndex as string]
);
const hasUnknownSecretsProvided = secretKeysInDB.length !== inputSecrets.length;
if (hasUnknownSecretsProvided) {
const keysMissingInDB = Object.keys(keyName2BlindIndex).filter((key) => !secretKeysInDB.includes(key));
throw new BadRequestError({
message: `Secret not found: blind index ${keysMissingInDB.join(",")}`
});
}
}
return { blindIndex2KeyName, keyName2BlindIndex, secrets };
};
// this is used when secret blind index already exist
// mainly for secret approval
const fnSecretBlindIndexCheckV2 = async ({ inputSecrets, folderId, userId }: TFnSecretBlindIndexCheckV2) => {
@@ -311,7 +172,8 @@ export const secretServiceFactory = ({
folderId,
isNew: true,
userId: actorId,
blindIndexCfg
blindIndexCfg,
secretDAL
});
// if user creating personal check its shared also exist
@@ -348,6 +210,10 @@ export const secretServiceFactory = ({
tags: inputSecret.tags
}
],
secretDAL,
secretVersionDAL,
secretTagDAL,
secretVersionTagDAL,
tx
})
);
@@ -395,7 +261,8 @@ export const secretServiceFactory = ({
folderId,
isNew: false,
blindIndexCfg,
userId: actorId
userId: actorId,
secretDAL
});
if (inputSecret.newSecretName && inputSecret.type === SecretType.Personal) {
throw new BadRequestError({ message: "Personal secret cannot change the key name" });
@@ -407,7 +274,8 @@ export const secretServiceFactory = ({
inputSecrets: [{ secretName: inputSecret.newSecretName }],
folderId,
isNew: true,
blindIndexCfg
blindIndexCfg,
secretDAL
});
newSecretNameBlindIndex = kN2NewBlindIndex[inputSecret.newSecretName];
}
@@ -454,6 +322,10 @@ export const secretServiceFactory = ({
}
}
],
secretDAL,
secretVersionDAL,
secretTagDAL,
secretVersionTagDAL,
tx
})
);
@@ -496,7 +368,8 @@ export const secretServiceFactory = ({
inputSecrets: [{ secretName: inputSecret.secretName }],
folderId,
isNew: false,
blindIndexCfg
blindIndexCfg,
secretDAL
});
const deletedSecret = await secretDAL.transaction(async (tx) =>
@@ -679,13 +552,14 @@ export const secretServiceFactory = ({
const folderId = folder.id;
const blindIndexCfg = await secretBlindIndexDAL.findOne({ projectId });
if (!blindIndexCfg) throw new BadRequestError({ message: "Blind index not found", name: "Update secret" });
if (!blindIndexCfg) throw new BadRequestError({ message: "Blind index not found", name: "Create secret" });
const { keyName2BlindIndex } = await fnSecretBlindIndexCheck({
inputSecrets,
folderId,
isNew: true,
blindIndexCfg
blindIndexCfg,
secretDAL
});
// get all tags
@@ -704,6 +578,10 @@ export const secretServiceFactory = ({
keyEncoding: SecretKeyEncoding.UTF8
})),
folderId,
secretDAL,
secretVersionDAL,
secretTagDAL,
secretVersionTagDAL,
tx
})
);
@@ -732,7 +610,7 @@ export const secretServiceFactory = ({
await projectDAL.checkProjectUpgradeStatus(projectId);
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create secret" });
if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Update secret" });
const folderId = folder.id;
const blindIndexCfg = await secretBlindIndexDAL.findOne({ projectId });
@@ -742,7 +620,8 @@ export const secretServiceFactory = ({
inputSecrets,
folderId,
isNew: false,
blindIndexCfg
blindIndexCfg,
secretDAL
});
// now find any secret that needs to update its name
@@ -752,7 +631,8 @@ export const secretServiceFactory = ({
inputSecrets: nameUpdatedSecrets,
folderId,
isNew: true,
blindIndexCfg
blindIndexCfg,
secretDAL
});
// get all tags
@@ -777,7 +657,11 @@ export const secretServiceFactory = ({
algorithm: SecretEncryptionAlgo.AES_256_GCM,
keyEncoding: SecretKeyEncoding.UTF8
}
}))
})),
secretDAL,
secretVersionDAL,
secretTagDAL,
secretVersionTagDAL
})
);
@@ -815,7 +699,8 @@ export const secretServiceFactory = ({
inputSecrets,
folderId,
isNew: false,
blindIndexCfg
blindIndexCfg,
secretDAL
});
const secretsDeleted = await secretDAL.transaction(async (tx) =>

View File

@@ -2,6 +2,14 @@ import { Knex } from "knex";
import { SecretType, TSecretBlindIndexes, TSecrets, TSecretsInsert, TSecretsUpdate } from "@app/db/schemas";
import { TProjectPermission } from "@app/lib/types";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
import { TSecretDALFactory } from "@app/services/secret/secret-dal";
import { TSecretVersionDALFactory } from "@app/services/secret/secret-version-dal";
import { TSecretVersionTagDALFactory } from "@app/services/secret/secret-version-tag-dal";
import { TSecretBlindIndexDALFactory } from "@app/services/secret-blind-index/secret-blind-index-dal";
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
import { TSecretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal";
type TPartialSecret = Pick<TSecrets, "id" | "secretReminderRepeatDays" | "secretReminderNote">;
@@ -181,12 +189,20 @@ export type TFnSecretBulkInsert = {
folderId: string;
tx?: Knex;
inputSecrets: Array<Omit<TSecretsInsert, "folderId"> & { tags?: string[] }>;
secretDAL: Pick<TSecretDALFactory, "insertMany">;
secretVersionDAL: Pick<TSecretVersionDALFactory, "insertMany">;
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecret">;
secretVersionTagDAL: Pick<TSecretVersionTagDALFactory, "insertMany">;
};
export type TFnSecretBulkUpdate = {
folderId: string;
projectId: string;
inputSecrets: { filter: Partial<TSecrets>; data: TSecretsUpdate & { tags?: string[] } }[];
secretDAL: Pick<TSecretDALFactory, "bulkUpdate">;
secretVersionDAL: Pick<TSecretVersionDALFactory, "insertMany">;
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecret" | "deleteTagsManySecret">;
secretVersionTagDAL: Pick<TSecretVersionTagDALFactory, "insertMany">;
tx?: Knex;
};
@@ -204,6 +220,7 @@ export type TFnSecretBlindIndexCheck = {
blindIndexCfg: TSecretBlindIndexes;
inputSecrets: Array<{ secretName: string; type?: SecretType }>;
isNew: boolean;
secretDAL: Pick<TSecretDALFactory, "findByBlindIndexes">;
};
// when blind index is already present
@@ -229,3 +246,66 @@ export type TRemoveSecretReminderDTO = {
secretId: string;
repeatDays: number;
};
// ---
export type TCreateManySecretsRawFnFactory = {
projectDAL: TProjectDALFactory;
projectBotDAL: TProjectBotDALFactory;
secretDAL: TSecretDALFactory;
secretVersionDAL: TSecretVersionDALFactory;
secretBlindIndexDAL: TSecretBlindIndexDALFactory;
secretTagDAL: TSecretTagDALFactory;
secretVersionTagDAL: TSecretVersionTagDALFactory;
folderDAL: TSecretFolderDALFactory;
};
export type TCreateManySecretsRawFn = {
projectId: string;
environment: string;
path: string;
secrets: {
secretName: string;
secretValue: string;
type: SecretType;
secretComment?: string;
skipMultilineEncoding?: boolean;
tags?: string[];
metadata?: {
source?: string;
};
}[];
userId?: string; // only relevant for personal secret(s)
};
export type TUpdateManySecretsRawFnFactory = {
projectDAL: TProjectDALFactory;
projectBotDAL: TProjectBotDALFactory;
secretDAL: TSecretDALFactory;
secretVersionDAL: TSecretVersionDALFactory;
secretBlindIndexDAL: TSecretBlindIndexDALFactory;
secretTagDAL: TSecretTagDALFactory;
secretVersionTagDAL: TSecretVersionTagDALFactory;
folderDAL: TSecretFolderDALFactory;
};
export type TUpdateManySecretsRawFn = {
projectId: string;
environment: string;
path: string;
secrets: {
secretName: string;
newSecretName?: string;
secretValue: string;
type: SecretType;
secretComment?: string;
skipMultilineEncoding?: boolean;
secretReminderRepeatDays?: number | null;
secretReminderNote?: string | null;
tags?: string[];
metadata?: {
source?: string;
};
}[];
userId?: string;
};

View File

@@ -299,10 +299,10 @@ type GetFoldersV1Response struct {
}
type CreateFolderV1Request struct {
FolderName string `json:"folderName"`
FolderName string `json:"name"`
WorkspaceId string `json:"workspaceId"`
Environment string `json:"environment"`
Directory string `json:"directory"`
Path string `json:"path"`
}
type CreateFolderV1Response struct {

View File

@@ -228,7 +228,9 @@ func secretTemplateFunction(accessToken string, existingEtag string, currentEtag
*currentEtag = res.Etag
}
return res.Secrets, nil
expandedSecrets := util.ExpandSecrets(res.Secrets, models.ExpandSecretsAuthentication{UniversalAuthAccessToken: accessToken}, "")
return expandedSecrets, nil
}
}
@@ -622,7 +624,7 @@ var agentCmd = &cobra.Command{
}
if !FileExists(configPath) && agentConfigInBase64 == "" {
log.Error().Msgf("No agent config file provided. Please provide a agent config file", configPath)
log.Error().Msgf("No agent config file provided at %v. Please provide a agent config file", configPath)
return
}

View File

@@ -59,7 +59,8 @@ var exportCmd = &cobra.Command{
util.HandleError(err, "Unable to parse flag")
}
infisicalToken, err := cmd.Flags().GetString("token")
infisicalToken, err := util.GetInfisicalServiceToken(cmd)
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
@@ -87,7 +88,9 @@ var exportCmd = &cobra.Command{
var output string
if shouldExpandSecrets {
secrets = util.ExpandSecrets(secrets, infisicalToken, "")
secrets = util.ExpandSecrets(secrets, models.ExpandSecretsAuthentication{
InfisicalToken: infisicalToken,
}, "")
}
secrets = util.FilterSecretsByTag(secrets, tagSlugs)
output, err = formatEnvs(secrets, format)

View File

@@ -36,7 +36,8 @@ var getCmd = &cobra.Command{
}
}
infisicalToken, err := cmd.Flags().GetString("token")
infisicalToken, err := util.GetInfisicalServiceToken(cmd)
if err != nil {
util.HandleError(err, "Unable to parse flag")
}

View File

@@ -62,7 +62,8 @@ var runCmd = &cobra.Command{
}
}
infisicalToken, err := cmd.Flags().GetString("token")
infisicalToken, err := util.GetInfisicalServiceToken(cmd)
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
@@ -110,7 +111,9 @@ var runCmd = &cobra.Command{
}
if shouldExpandSecrets {
secrets = util.ExpandSecrets(secrets, infisicalToken, projectConfigDir)
secrets = util.ExpandSecrets(secrets, models.ExpandSecretsAuthentication{
InfisicalToken: infisicalToken,
}, projectConfigDir)
}
secretsByKey := getSecretsByKeys(secrets)

View File

@@ -38,7 +38,12 @@ var secretsCmd = &cobra.Command{
}
}
infisicalToken, err := cmd.Flags().GetString("token")
infisicalToken, err := util.GetInfisicalServiceToken(cmd)
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
@@ -80,7 +85,9 @@ var secretsCmd = &cobra.Command{
}
if shouldExpandSecrets {
secrets = util.ExpandSecrets(secrets, infisicalToken, "")
secrets = util.ExpandSecrets(secrets, models.ExpandSecretsAuthentication{
InfisicalToken: infisicalToken,
}, "")
}
visualize.PrintAllSecretDetails(secrets)
@@ -391,7 +398,8 @@ func getSecretsByNames(cmd *cobra.Command, args []string) {
}
}
infisicalToken, err := cmd.Flags().GetString("token")
infisicalToken, err := util.GetInfisicalServiceToken(cmd)
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
@@ -406,6 +414,11 @@ func getSecretsByNames(cmd *cobra.Command, args []string) {
util.HandleError(err, "Unable to parse path flag")
}
showOnlyValue, err := cmd.Flags().GetBool("raw-value")
if err != nil {
util.HandleError(err, "Unable to parse path flag")
}
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, SecretsPath: secretsPath}, "")
if err != nil {
util.HandleError(err, "To fetch all secrets")
@@ -427,7 +440,15 @@ func getSecretsByNames(cmd *cobra.Command, args []string) {
}
}
visualize.PrintAllSecretDetails(requestedSecrets)
if showOnlyValue && len(requestedSecrets) > 1 {
util.PrintErrorMessageAndExit("--raw-value only works with one secret.")
}
if showOnlyValue {
fmt.Printf(requestedSecrets[0].Value)
} else {
visualize.PrintAllSecretDetails(requestedSecrets)
}
Telemetry.CaptureEvent("cli-command:secrets get", posthog.NewProperties().Set("secretCount", len(secrets)).Set("version", util.CLI_VERSION))
}
@@ -445,7 +466,8 @@ func generateExampleEnv(cmd *cobra.Command, args []string) {
util.HandleError(err, "Unable to parse flag")
}
infisicalToken, err := cmd.Flags().GetString("token")
infisicalToken, err := util.GetInfisicalServiceToken(cmd)
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
@@ -661,6 +683,7 @@ func init() {
secretsGetCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token")
secretsCmd.AddCommand(secretsGetCmd)
secretsGetCmd.Flags().String("path", "/", "get secrets within a folder path")
secretsGetCmd.Flags().Bool("raw-value", false, "Returns only the value of secret, only works with one secret")
secretsCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets, if any, with the same name over shared secrets")
secretsCmd.AddCommand(secretsSetCmd)

View File

@@ -21,11 +21,12 @@ type LoggedInUser struct {
}
type SingleEnvironmentVariable struct {
Key string `json:"key"`
Value string `json:"value"`
Type string `json:"type"`
ID string `json:"_id"`
Tags []struct {
Key string `json:"key"`
WorkspaceId string `json:"workspace"`
Value string `json:"value"`
Type string `json:"type"`
ID string `json:"_id"`
Tags []struct {
ID string `json:"_id"`
Name string `json:"name"`
Slug string `json:"slug"`
@@ -68,6 +69,7 @@ type GetAllSecretsParameters struct {
Environment string
EnvironmentPassedViaFlag bool
InfisicalToken string
UniversalAuthAccessToken string
TagSlugs string
WorkspaceId string
SecretsPath string
@@ -96,3 +98,8 @@ type DeleteFolderParameters struct {
FolderPath string
InfisicalToken string
}
type ExpandSecretsAuthentication struct {
InfisicalToken string
UniversalAuthAccessToken string
}

View File

@@ -154,7 +154,7 @@ func CreateFolder(params models.CreateFolderParameters) (models.SingleFolder, er
WorkspaceId: params.WorkspaceId,
Environment: params.Environment,
FolderName: params.FolderName,
Directory: params.FolderPath,
Path: params.FolderPath,
}
apiResponse, err := api.CallCreateFolderV1(httpClient, createFolderRequest)

View File

@@ -11,6 +11,7 @@ import (
"strings"
"github.com/Infisical/infisical-merge/packages/models"
"github.com/spf13/cobra"
)
type DecodedSymmetricEncryptionDetails = struct {
@@ -63,6 +64,20 @@ func IsSecretTypeValid(s string) bool {
return false
}
func GetInfisicalServiceToken(cmd *cobra.Command) (serviceToken string, err error) {
infisicalToken, err := cmd.Flags().GetString("token")
if infisicalToken == "" {
infisicalToken = os.Getenv(INFISICAL_TOKEN_NAME)
}
if err != nil {
return "", err
}
return infisicalToken, nil
}
// Checks if the passed in email already exists in the users slice
func ConfigContainsEmail(users []models.LoggedInUser, email string) bool {
for _, value := range users {

View File

@@ -179,7 +179,7 @@ func GetPlainTextSecretsViaMachineIdentity(accessToken string, workspaceId strin
}
for _, secret := range rawSecrets.Secrets {
plainTextSecrets = append(plainTextSecrets, models.SingleEnvironmentVariable{Key: secret.SecretKey, Value: secret.SecretValue})
plainTextSecrets = append(plainTextSecrets, models.SingleEnvironmentVariable{Key: secret.SecretKey, Value: secret.SecretValue, WorkspaceId: secret.Workspace})
}
// if includeImports {
@@ -248,11 +248,8 @@ func FilterSecretsByTag(plainTextSecrets []models.SingleEnvironmentVariable, tag
}
func GetAllEnvironmentVariables(params models.GetAllSecretsParameters, projectConfigFilePath string) ([]models.SingleEnvironmentVariable, error) {
var infisicalToken string
if params.InfisicalToken == "" {
infisicalToken = os.Getenv(INFISICAL_TOKEN_NAME)
} else {
infisicalToken = params.InfisicalToken
params.InfisicalToken = os.Getenv(INFISICAL_TOKEN_NAME)
}
isConnected := CheckIsConnectedToInternet()
@@ -260,7 +257,7 @@ func GetAllEnvironmentVariables(params models.GetAllSecretsParameters, projectCo
// var serviceTokenDetails api.GetServiceTokenDetailsResponse
var errorToReturn error
if infisicalToken == "" {
if params.InfisicalToken == "" && params.UniversalAuthAccessToken == "" {
if isConnected {
log.Debug().Msg("GetAllEnvironmentVariables: Connected to internet, checking logged in creds")
@@ -306,12 +303,6 @@ func GetAllEnvironmentVariables(params models.GetAllSecretsParameters, projectCo
infisicalDotJson.WorkspaceId = params.WorkspaceId
}
// // Verify environment
// err = ValidateEnvironmentName(params.Environment, workspaceFile.WorkspaceId, loggedInUserDetails.UserCredentials)
// if err != nil {
// return nil, fmt.Errorf("unable to validate environment name because [err=%s]", err)
// }
secretsToReturn, errorToReturn = GetPlainTextSecretsViaJTW(loggedInUserDetails.UserCredentials.JTWToken, loggedInUserDetails.UserCredentials.PrivateKey, infisicalDotJson.WorkspaceId,
params.Environment, params.TagSlugs, params.SecretsPath, params.IncludeImport)
log.Debug().Msgf("GetAllEnvironmentVariables: Trying to fetch secrets JTW token [err=%s]", errorToReturn)
@@ -332,91 +323,19 @@ func GetAllEnvironmentVariables(params models.GetAllSecretsParameters, projectCo
}
} else {
log.Debug().Msg("Trying to fetch secrets using service token")
secretsToReturn, _, errorToReturn = GetPlainTextSecretsViaServiceToken(infisicalToken, params.Environment, params.SecretsPath, params.IncludeImport)
}
if params.InfisicalToken != "" {
log.Debug().Msg("Trying to fetch secrets using service token")
secretsToReturn, _, errorToReturn = GetPlainTextSecretsViaServiceToken(params.InfisicalToken, params.Environment, params.SecretsPath, params.IncludeImport)
} else if params.UniversalAuthAccessToken != "" {
log.Debug().Msg("Trying to fetch secrets using universal auth")
res, err := GetPlainTextSecretsViaMachineIdentity(params.UniversalAuthAccessToken, params.WorkspaceId, params.Environment, params.SecretsPath, params.IncludeImport)
return secretsToReturn, errorToReturn
}
// func ValidateEnvironmentName(environmentName string, workspaceId string, userLoggedInDetails models.UserCredentials) error {
// httpClient := resty.New()
// httpClient.SetAuthToken(userLoggedInDetails.JTWToken).
// SetHeader("Accept", "application/json")
// response, err := api.CallGetAccessibleEnvironments(httpClient, api.GetAccessibleEnvironmentsRequest{WorkspaceId: workspaceId})
// if err != nil {
// return err
// }
// listOfEnvSlugs := []string{}
// mapOfEnvSlugs := make(map[string]interface{})
// for _, environment := range response.AccessibleEnvironments {
// listOfEnvSlugs = append(listOfEnvSlugs, environment.Slug)
// mapOfEnvSlugs[environment.Slug] = environment
// }
// _, exists := mapOfEnvSlugs[environmentName]
// if !exists {
// HandleError(fmt.Errorf("the environment [%s] does not exist in project with [id=%s]. Only [%s] are available", environmentName, workspaceId, strings.Join(listOfEnvSlugs, ",")))
// }
// return nil
// }
func getExpandedEnvVariable(secrets []models.SingleEnvironmentVariable, variableWeAreLookingFor string, hashMapOfCompleteVariables map[string]string, hashMapOfSelfRefs map[string]string) string {
if value, found := hashMapOfCompleteVariables[variableWeAreLookingFor]; found {
return value
}
for _, secret := range secrets {
if secret.Key == variableWeAreLookingFor {
regex := regexp.MustCompile(`\${([^\}]*)}`)
variablesToPopulate := regex.FindAllString(secret.Value, -1)
// case: variable is a constant so return its value
if len(variablesToPopulate) == 0 {
return secret.Value
}
valueToEdit := secret.Value
for _, variableWithSign := range variablesToPopulate {
variableWithoutSign := strings.Trim(variableWithSign, "}")
variableWithoutSign = strings.Trim(variableWithoutSign, "${")
// case: reference to self
if variableWithoutSign == secret.Key {
hashMapOfSelfRefs[variableWithoutSign] = variableWithoutSign
continue
} else {
var expandedVariableValue string
if preComputedVariable, found := hashMapOfCompleteVariables[variableWithoutSign]; found {
expandedVariableValue = preComputedVariable
} else {
expandedVariableValue = getExpandedEnvVariable(secrets, variableWithoutSign, hashMapOfCompleteVariables, hashMapOfSelfRefs)
hashMapOfCompleteVariables[variableWithoutSign] = expandedVariableValue
}
// If after expanding all the vars above, is the current var a self ref? if so no replacement needed for it
if _, found := hashMapOfSelfRefs[variableWithoutSign]; found {
continue
} else {
valueToEdit = strings.ReplaceAll(valueToEdit, variableWithSign, expandedVariableValue)
}
}
}
return valueToEdit
} else {
continue
errorToReturn = err
secretsToReturn = res.Secrets
}
}
return "${" + variableWeAreLookingFor + "}"
return secretsToReturn, errorToReturn
}
var secRefRegex = regexp.MustCompile(`\${([^\}]*)}`)
@@ -428,7 +347,7 @@ func recursivelyExpandSecret(expandedSecs map[string]string, interpolatedSecs ma
interpolatedVal, ok := interpolatedSecs[key]
if !ok {
HandleError(fmt.Errorf("Could not find refered secret - %s", key), "Kindly check whether its provided")
HandleError(fmt.Errorf("could not find refered secret - %s", key), "Kindly check whether its provided")
}
refs := secRefRegex.FindAllStringSubmatch(interpolatedVal, -1)
@@ -467,7 +386,7 @@ func getSecretsByKeys(secrets []models.SingleEnvironmentVariable) map[string]mod
return secretMapByName
}
func ExpandSecrets(secrets []models.SingleEnvironmentVariable, infisicalToken string, projectConfigPathDir string) []models.SingleEnvironmentVariable {
func ExpandSecrets(secrets []models.SingleEnvironmentVariable, auth models.ExpandSecretsAuthentication, projectConfigPathDir string) []models.SingleEnvironmentVariable {
expandedSecs := make(map[string]string)
interpolatedSecs := make(map[string]string)
// map[env.secret-path][keyname]Secret
@@ -499,8 +418,18 @@ func ExpandSecrets(secrets []models.SingleEnvironmentVariable, infisicalToken st
uniqKey := fmt.Sprintf("%s.%s", env, secPathDot)
if crossRefSec, ok := crossEnvRefSecs[uniqKey]; !ok {
var refSecs []models.SingleEnvironmentVariable
var err error
// if not in cross reference cache, fetch it from server
refSecs, err := GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: env, InfisicalToken: infisicalToken, SecretsPath: secPath}, projectConfigPathDir)
if auth.InfisicalToken != "" {
refSecs, err = GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: env, InfisicalToken: auth.InfisicalToken, SecretsPath: secPath}, projectConfigPathDir)
} else if auth.UniversalAuthAccessToken != "" {
refSecs, err = GetAllEnvironmentVariables((models.GetAllSecretsParameters{Environment: env, UniversalAuthAccessToken: auth.UniversalAuthAccessToken, SecretsPath: secPath, WorkspaceId: sec.WorkspaceId}), projectConfigPathDir)
} else {
HandleError(errors.New("no authentication provided"), "Please provide authentication to fetch secrets")
}
if err != nil {
HandleError(err, fmt.Sprintf("Could not fetch secrets in environment: %s secret-path: %s", env, secPath), "If you are using a service token to fetch secrets, please ensure it is valid")
}
@@ -508,6 +437,7 @@ func ExpandSecrets(secrets []models.SingleEnvironmentVariable, infisicalToken st
// save it to avoid calling api again for same environment and folder path
crossEnvRefSecs[uniqKey] = refSecsByKey
return refSecsByKey[secKey].Value
} else {
return crossRefSec[secKey].Value
}

View File

@@ -8,24 +8,28 @@ infisical secrets
```
## Description
This command enables you to perform CRUD (create, read, update, delete) operations on secrets within your Infisical project. With it, you can view, create, update, and delete secrets in your environment.
### Sub-commands
### Sub-commands
<Accordion title="infisical secrets" defaultOpen="true">
Use this command to print out all of the secrets in your project
```bash
$ infisical secrets
```
```bash
$ infisical secrets
```
### Environment variables
### Environment variables
<Accordion title="INFISICAL_TOKEN">
Used to fetch secrets via a [service token](/documentation/platform/token) apposed to logged in credentials. Simply, export this variable in the terminal before running this command.
```bash
# Example
# Example
export INFISICAL_TOKEN=st.63e03c4a97cb4a747186c71e.ed5b46a34c078a8f94e8228f4ab0ff97.4f7f38034811995997d72badf44b42ec
```
</Accordion>
<Accordion title="INFISICAL_DISABLE_UPDATE_CHECK">
@@ -34,22 +38,26 @@ This command enables you to perform CRUD (create, read, update, delete) operatio
To use, simply export this variable in the terminal before running this command.
```bash
# Example
# Example
export INFISICAL_DISABLE_UPDATE_CHECK=true
```
</Accordion>
### Flags
### Flags
<Accordion title="--expand">
Parse shell parameter expansions in your secrets
Default value: `true`
</Accordion>
<Accordion title="--env">
Used to select the environment name on which actions should be taken on
Default value: `dev`
</Accordion>
<Accordion title="--path">
The `--path` flag indicates which project folder secrets will be injected from.
@@ -58,6 +66,7 @@ This command enables you to perform CRUD (create, read, update, delete) operatio
# Example
infisical secrets --path="/" --env=dev
```
</Accordion>
</Accordion>
@@ -65,38 +74,55 @@ This command enables you to perform CRUD (create, read, update, delete) operatio
<Accordion title="infisical secrets get">
This command allows you selectively print the requested secrets by name
```bash
$ infisical secrets get <secret-name-a> <secret-name-b> ...
```bash
$ infisical secrets get <secret-name-a> <secret-name-b> ...
# Example
$ infisical secrets get DOMAIN
# Example
$ infisical secrets get DOMAIN
```
```
### Flags
### Flags
<Accordion title="--env">
Used to select the environment name on which actions should be taken on
Default value: `dev`
</Accordion>
<Accordion title="--raw-value">
Used to print the plain value of a single requested secret without any table style.
Default value: `false`
Example: `infisical secrets get DOMAIN --raw-value`
<Tip>
When running in CI/CD environments or in a script, set `INFISICAL_DISABLE_UPDATE_CHECK` env to `true`. This will help hide any CLI update messages and only show the secret value.
</Tip>
</Accordion>
</Accordion>
<Accordion title="infisical secrets set">
This command allows you to set or update secrets in your environment. If the secret key provided already exists, its value will be updated with the new value.
This command allows you to set or update secrets in your environment. If the secret key provided already exists, its value will be updated with the new value.
If the secret key does not exist, a new secret will be created using both the key and value provided.
```bash
$ infisical secrets set <key1=value1> <key2=value2>...
## Example
## Example
$ infisical secrets set STRIPE_API_KEY=sjdgwkeudyjwe DOMAIN=example.com HASH=jebhfbwe
```
### Flags
### Flags
<Accordion title="--env">
Used to select the environment name on which actions should be taken on
Default value: `dev`
</Accordion>
<Accordion title="--path">
Used to select the project folder in which the secrets will be set. This is useful when creating new secrets under a particular path.
@@ -105,43 +131,48 @@ $ infisical secrets set STRIPE_API_KEY=sjdgwkeudyjwe DOMAIN=example.com HASH=jeb
# Example
infisical secrets set DOMAIN=example.com --path="common/backend"
```
</Accordion>
</Accordion>
<Accordion title="infisical secrets delete">
This command allows you to delete secrets by their name(s).
```bash
$ infisical secrets delete <keyName1> <keyName2>...
```bash
$ infisical secrets delete <keyName1> <keyName2>...
## Example
$ infisical secrets delete STRIPE_API_KEY DOMAIN HASH
```
## Example
$ infisical secrets delete STRIPE_API_KEY DOMAIN HASH
```
### Flags
### Flags
<Accordion title="--env">
Used to select the environment name on which actions should be taken on
Default value: `dev`
</Accordion>
<Accordion title="--path">
The `--path` flag indicates which project folder secrets will be injected from.
The `--path` flag indicates which project folder secrets will be injected from.
```bash
# Example
infisical secrets delete <keyName1> <keyName2>... --path="/"
```
</Accordion>
</Accordion>
<Accordion title="infisical secrets folders">
This command allows you to fetch, create and delete folders from within a path from a given project.
```bash
$ infisical secrets folders
```
```bash
$ infisical secrets folders
```
### sub commands
### sub commands
<Accordion title="get">
Used to fetch all folders within a path in a given project
```
@@ -179,6 +210,7 @@ $ infisical secrets set STRIPE_API_KEY=sjdgwkeudyjwe DOMAIN=example.com HASH=jeb
Default value: ``
</Accordion>
</Accordion>
<Accordion title="delete">
@@ -194,10 +226,11 @@ $ infisical secrets set STRIPE_API_KEY=sjdgwkeudyjwe DOMAIN=example.com HASH=jeb
</Accordion>
<Accordion title="--name">
Name of the folder to be deleted within selected `--path`
Name of the folder to be deleted within selected `--path`
Default value: ``
</Accordion>
</Accordion>
</Accordion>
@@ -210,14 +243,16 @@ To place default values in your example .env file, you can simply include the sy
```bash
$ infisical secrets generate-example-env
## Example
## Example
$ infisical secrets generate-example-env > .example-env
```
### Flags
### Flags
<Accordion title="--env">
Used to select the environment name on which actions should be taken on
Default value: `dev`
</Accordion>
</Accordion>

View File

@@ -10,7 +10,7 @@ This means that updating the value of a base secret propagates directly to other
<Note>
Currently, the secret referencing feature is only supported by the
[Infisical CLI](/cli/overview) and [native integrations](/integrations/overview).
[Infisical CLI](/cli/overview), [native integrations](/integrations/overview) and [Infisical Agent](/infisical-agent/overview).
We intend to add support for it to the [Node SDK](https://infisical.com/docs/sdks/languages/node),
[Python SDK](https://infisical.com/docs/sdks/languages/python), and [Java SDK](https://infisical.com/docs/sdks/languages/java) this quarter.

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 179 KiB

After

Width:  |  Height:  |  Size: 533 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 371 KiB

After

Width:  |  Height:  |  Size: 700 KiB

View File

@@ -0,0 +1,164 @@
---
title: 'Docker Swarm'
description: "How to manage secrets in Docker Swarm services"
---
In this guide, we'll demonstrate how to use Infisical for managing secrets within Docker Swarm.
Specifically, we'll set up a sidecar container using the [Infisical Agent](/infisical-agent/overview), which authenticates with Infisical to retrieve secrets and access tokens.
These secrets are then stored in a shared volume accessible by other services in your Docker Swarm.
## Prerequisites
- Infisical account
- Docker version 20.10.24 or newer
- Basic knowledge of Docker Swarm
- [Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) installed on your system
- Familiarity with the [Infisical Agent](/infisical-agent/overview)
## Objective
Our goal is to deploy an Nginx instance in your Docker Swarm cluster, configured to display Infisical secrets on its landing page. This will provide hands-on experience in fetching and utilizing secrets from Infisical within Docker Swarm. The principles demonstrated here are also applicable to Docker Compose deployments.
<Steps>
<Step title="Cloning the Guide Assets Repository">
Start by cloning the [Infisical guide assets repository](https://github.com/Infisical/infisical-guides.git) from Github. This repository includes necessary assets for this and other Infisical guides. Focus on the `docker-swarm-with-agent` sub-directory, which we'll use as our working directory.
</Step>
<Step title="Setting Up Authentication with Infisical">
To allow the agent to fetch your Infisical secrets, choose an authentication method for the agent. For this guide, we will use [Universal Auth](/documentation/platform/identities/universal-auth) for authentication. Follow the instructions [here](/documentation/platform/identities/universal-auth) to generate a client ID and client secret.
</Step>
<Step title="Entering Universal Auth Credentials">
Copy the client ID and client secret obtained in the previous step into the `client-id` and `client-secret` text files, respectively.
</Step>
<Step title="Configuring the Infisical Agent">
The Infisical Agent will authenticate using Universal Auth and retrieve secrets for rendering as specified in the template(s).
Adjust the `polling-interval` to control the frequency of secret updates.
In the example template, the secrets are rendered as an HTML page, which will be set as Nginx's home page to demonstrate successful secret retrieval and utilization.
<Tip>
Remember to add your project id, environment slug and path of corresponding Infisical project to the secret template.
</Tip>
<Tabs>
<Tab title="Agent Configuration">
```yaml infisical-agent-config
infisical:
address: "https://app.infisical.com"
auth:
type: "universal-auth"
config:
client-id: "/run/secrets/infisical-universal-auth-client-id"
client-secret: "/run/secrets/infisical-universal-auth-client-secret"
remove_client_secret_on_read: false
sinks:
- type: "file"
config:
path: "/infisical-secrets/access-token"
templates:
- source-path: /run/secrets/nginx-home-page-template
destination-path: /infisical-secrets/index.html
config:
polling-interval: 60s
```
<Info>
Some paths contain `/run/secrets/` because the contents of those files reside in a [Docker secret](https://docs.docker.com/engine/swarm/secrets/#how-docker-manages-secrets).
</Info>
</Tab>
<Tab title="Secret Template for Agent">
```html nginx-home-page-template
<!DOCTYPE html>
<html lang="en">
<body>
<h1>This file is rendered by Infisical agent template engine</h1>
<p>Here are the secrets that have been fetched from Infisical and stored in your volume mount</p>
<ol>
{{- with secret "7df67a5f-d26a-4988-a375-7153c08149da" "dev" "/" }}
{{- range . }}
<li>{{ .Key }}={{ .Value }}</li>
{{- end }}
{{- end }}
</ol>
</body>
</html>
```
</Tab>
</Tabs>
</Step>
<Step title="Creating the Docker Compose File">
Define the `infisical-agent` and `nginx` services in your Docker Compose file. `infisical-agent` will handle secret retrieval and storage. These secrets are stored in a volume, accessible by other services like Nginx.
```yaml docker-compose.yaml
version: "3.1"
services:
infisical-agent:
container_name: infisical-agnet
image: infisical/cli:0.18.0
command: agent --config=/run/secrets/infisical-agent-config
volumes:
- infisical-agent:/infisical-secrets
secrets:
- infisical-universal-auth-client-id
- infisical-universal-auth-client-secret
- infisical-agent-config
- nginx-home-page-template
networks:
- infisical_network
nginx:
image: nginx:latest
ports:
- "80:80"
volumes:
- infisical-agent:/usr/share/nginx/html
networks:
- infisical_network
volumes:
infisical-agent:
secrets:
infisical-universal-auth-client-id:
file: ./client-id
infisical-universal-auth-client-secret:
file: ./client-secret
infisical-agent-config:
file: ./infisical-agent-config
nginx-home-page-template:
file: ./nginx-home-page-template
networks:
infisical_network:
```
</Step>
<Step title="Initializing Docker Swarm">
```
docker swarm init
```
</Step>
<Step title="Deploying the Docker Stack">
```
docker stack deploy -c docker-compose.yaml agent-demo
```
</Step>
<Step title="Verifying Secret Consumption">
To confirm that secrets are properly rendered and accessible, navigate to `http://localhost`. You should see the Infisical secrets displayed on the Nginx landing page.
![Nginx displaying Infisical secrets](/images/docker-swarm-secrets-complete.png)
</Step>
<Step title="Clean up">
```
docker stack rm agent-demo
```
</Step>
</Steps>
## Considerations
- Secret Updates: Applications that access secrets directly from the volume mount will receive updates in real-time, in accordance with the `polling-interval` set in agent config.
- In-Memory Secrets: If your application loads secrets into memory, the new secrets will be available to the application on the next deployment.

View File

@@ -30,6 +30,17 @@ description: "How to sync secrets from Infisical to Heroku"
Select which Infisical environment secrets you want to sync to which Heroku app and press create integration to start syncing secrets to Heroku.
![integrations heroku](../../images/integrations/heroku/integrations-heroku-create.png)
Here's some guidance on each field:
- Project Environment: The environment in the current Infisical project from which you want to sync secrets from.
- Secrets Path: The path in the current Infisical project from which you want to sync secrets from such as `/` (for secrets that do not reside in a folder) or `/foo/bar` (for secrets nested in a folder, in this case a folder called `bar` in another folder called `foo`).
- Heroku App: The application in Heroku that you want to sync secrets to.
- Initial Sync Behavior (default is **Import - Prefer values from Infisical**): The behavior of the first sync operation triggered after creating the integration.
- **No Import - Overwrite all values in Heroku**: Sync secrets and overwrite any existing secrets in Heroku.
- **Import - Prefer values from Infisical**: Import secrets from Heroku to Infisical; if a secret with the same name already exists in Infisical, do nothing. Afterwards, sync secrets to Heroku.
- **Import - Prefer values from Heroku**: Import secrets from Heroku to Infisical; if a secret with the same name already exists in Infisical, replace its value with the one from Heroku. Afterwards, sync secrets to Heroku.
![integrations heroku](../../images/integrations/heroku/integrations-heroku.png)
</Step>
</Steps>

View File

@@ -228,12 +228,27 @@
{
"group": "Agent",
"pages": [
"infisical-agent/overview"
"infisical-agent/overview",
{
"group": "Use cases",
"pages": [
"infisical-agent/guides/docker-swarm-with-agent",
"integrations/platforms/ecs-with-agent"
]
}
]
},
{
"group": "Infrastructure Integrations",
"pages": [
{
"group": "Container orchestrators",
"pages": [
"integrations/platforms/kubernetes",
"infisical-agent/guides/docker-swarm-with-agent",
"integrations/platforms/ecs-with-agent"
]
},
{
"group": "Docker",
"pages": [
@@ -243,10 +258,8 @@
"integrations/platforms/docker-compose"
]
},
"integrations/platforms/kubernetes",
"integrations/frameworks/terraform",
"integrations/platforms/ansible",
"integrations/platforms/ecs-with-agent"
"integrations/platforms/ansible"
]
},
{

View File

@@ -1,5 +1,5 @@
{
"name": "npm-proj-1709146141702-0.772936286416932EMIzNi",
"name": "frontend",
"lockfileVersion": 3,
"requires": true,
"packages": {
@@ -31,6 +31,7 @@
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-popper": "^1.1.3",
"@radix-ui/react-progress": "^1.0.3",
"@radix-ui/react-radio-group": "^1.1.3",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-tabs": "^1.0.4",
@@ -68,7 +69,7 @@
"next": "^12.3.4",
"nprogress": "^0.2.0",
"picomatch": "^2.3.1",
"posthog-js": "^1.105.4",
"posthog-js": "^1.105.6",
"query-string": "^7.1.3",
"react": "^17.0.2",
"react-beautiful-dnd": "^13.1.1",
@@ -5223,6 +5224,38 @@
}
}
},
"node_modules/@radix-ui/react-radio-group": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.1.3.tgz",
"integrity": "sha512-x+yELayyefNeKeTx4fjK6j99Fs6c4qKm3aY38G3swQVTN6xMpsrbigC0uHs2L//g8q4qR7qOcww8430jJmi2ag==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/primitive": "1.0.1",
"@radix-ui/react-compose-refs": "1.0.1",
"@radix-ui/react-context": "1.0.1",
"@radix-ui/react-direction": "1.0.1",
"@radix-ui/react-presence": "1.0.1",
"@radix-ui/react-primitive": "1.0.3",
"@radix-ui/react-roving-focus": "1.0.4",
"@radix-ui/react-use-controllable-state": "1.0.1",
"@radix-ui/react-use-previous": "1.0.1",
"@radix-ui/react-use-size": "1.0.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-roving-focus": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.4.tgz",
@@ -19065,9 +19098,9 @@
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="
},
"node_modules/posthog-js": {
"version": "1.105.4",
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.105.4.tgz",
"integrity": "sha512-hazxQYi4nxSqktu0Hh1xCV+sJCpN8mp5E5Ei/cfEa2nsb13xQbzn81Lf3VIDA0xMU1mXxNRStntlY267eQVC/w==",
"version": "1.105.6",
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.105.6.tgz",
"integrity": "sha512-5ITXsh29XIuNohHLy21nawGnfFZDpyt+yfnWge9sJl5yv0nNuoUmLiDgw1tJafoqGrfd5CUasKyzSI21HxsSeQ==",
"dependencies": {
"fflate": "^0.4.8",
"preact": "^10.19.3"

View File

@@ -39,6 +39,7 @@
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-popper": "^1.1.3",
"@radix-ui/react-progress": "^1.0.3",
"@radix-ui/react-radio-group": "^1.1.3",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-tabs": "^1.0.4",
@@ -76,7 +77,7 @@
"next": "^12.3.4",
"nprogress": "^0.2.0",
"picomatch": "^2.3.1",
"posthog-js": "^1.105.4",
"posthog-js": "^1.105.6",
"query-string": "^7.1.3",
"react": "^17.0.2",
"react-beautiful-dnd": "^13.1.1",

View File

@@ -0,0 +1,40 @@
/* eslint-disable jsx-a11y/label-has-associated-control */
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
import { twMerge } from "tailwind-merge";
export type RadioGroupProps = RadioGroupPrimitive.RadioGroupProps;
// Note this component is not customizable (Heroku integration and potentially other pages depend on it)
export const RadioGroup = ({ className, children, ...props }: RadioGroupProps) => (
<RadioGroupPrimitive.Root
className={twMerge("flex flex-row gap-5 px-6 mb-6", className)}
defaultValue="App"
aria-label="View density"
{...props}
>
<div className="flex items-center">
<RadioGroupPrimitive.Item
className="bg-bunker-400/20 w-[20px] h-[20px] rounded-full hover:bg-bunker-400/40 border border-bunker-400/60 duration-200 outline-none cursor-default"
value="App"
id="r1"
>
<RadioGroupPrimitive.Indicator className="flex items-center justify-center w-full h-full relative after:content-[''] after:block after:w-[11px] after:h-[11px] after:rounded-[50%] after:bg-primary" />
</RadioGroupPrimitive.Item>
<label className="text-bunker-200 text-sm leading-none pl-2" htmlFor="r1">
App
</label>
</div>
<div className="flex items-center">
<RadioGroupPrimitive.Item
className="bg-bunker-400/20 w-[22px] h-[22px] rounded-full hover:bg-bunker-400/40 border border-bunker-400/60 duration-200 outline-none cursor-default"
value="Pipeline"
id="r2"
>
<RadioGroupPrimitive.Indicator className="flex items-center justify-center w-full h-full relative after:content-[''] after:block after:w-[13px] after:h-[13px] after:rounded-[50%] after:bg-primary" />
</RadioGroupPrimitive.Item>
<label className="text-bunker-200 text-sm leading-none pl-2" htmlFor="r2">
Pipeline
</label>
</div>
</RadioGroupPrimitive.Root>
);

View File

@@ -0,0 +1,2 @@
export type { RadioGroupProps } from "./RadioGroup";
export { RadioGroup } from "./RadioGroup";

View File

@@ -8,6 +8,7 @@ import {
BitBucketWorkspace,
ChecklyGroup,
Environment,
HerokuPipelineCoupling,
IntegrationAuth,
NorthflankSecretGroup,
Org,
@@ -63,6 +64,8 @@ const integrationAuthKeys = {
environmentId: string;
scope: "job" | "application" | "container";
}) => [{ integrationAuthId, environmentId, scope }, "integrationAuthQoveryScopes"] as const,
getIntegrationAuthHerokuPipelines: ({ integrationAuthId }: { integrationAuthId: string; }) =>
[{ integrationAuthId}, "integrationAuthHerokuPipelines"] as const,
getIntegrationAuthRailwayEnvironments: ({
integrationAuthId,
appId
@@ -289,6 +292,20 @@ const fetchIntegrationAuthQoveryScopes = async ({
return undefined;
};
const fetchIntegrationAuthHerokuPipelines = async ({ integrationAuthId }: {
integrationAuthId: string;
}) => {
const {
data: { pipelines }
} = await apiRequest.get<{ pipelines: HerokuPipelineCoupling[] }>(
`/api/v1/integration-auth/${integrationAuthId}/heroku/pipelines`
);
console.log(99999, pipelines)
return pipelines;
};
const fetchIntegrationAuthRailwayEnvironments = async ({
integrationAuthId,
appId
@@ -540,6 +557,23 @@ export const useGetIntegrationAuthQoveryScopes = ({
});
};
export const useGetIntegrationAuthHerokuPipelines = ({
integrationAuthId
}: {
integrationAuthId: string;
}) => {
return useQuery({
queryKey: integrationAuthKeys.getIntegrationAuthHerokuPipelines({
integrationAuthId
}),
queryFn: () =>
fetchIntegrationAuthHerokuPipelines({
integrationAuthId
}),
enabled: true
});
};
export const useGetIntegrationAuthRailwayEnvironments = ({
integrationAuthId,
appId

View File

@@ -17,6 +17,17 @@ export type App = {
secretGroups?: string[];
};
export type Pipeline = {
pipelineId: string;
name: string;
};
export type HerokuPipelineCoupling = {
app: { appId: string };
stage: string;
pipeline: { pipelineId: string; name: string };
};
export type Team = {
name: string;
teamId: string;

View File

@@ -61,6 +61,7 @@ export const useCreateIntegration = () => {
metadata?: {
secretPrefix?: string;
secretSuffix?: string;
initialSyncBehavior?: string;
}
}) => {
const { data: { integration } } = await apiRequest.post("/api/v1/integration", {

View File

@@ -33,9 +33,16 @@ export type TIntegration = {
__v: number;
metadata?: {
secretSuffix?: string;
syncBehavior?: IntegrationSyncBehavior;
scope: string;
org: string;
project: string;
environment: string;
};
};
export enum IntegrationSyncBehavior {
OVERWRITE_TARGET = "overwrite-target",
PREFER_TARGET = "prefer-target",
PREFER_SOURCE = "prefer-source"
}

View File

@@ -1,8 +1,25 @@
import { useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import Head from "next/head";
import Image from "next/image";
import Link from "next/link";
import { useRouter } from "next/router";
import {
faArrowUpRightFromSquare,
faBookOpen,
faBugs,
// faCircleInfo
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { yupResolver } from "@hookform/resolvers/yup";
import queryString from "query-string";
// import { useGetIntegrationAuthHerokuPipelines } from "@app/hooks/api/integrationAuth/queries";
// import { App, Pipeline } from "@app/hooks/api/integrationAuth/types";
import * as yup from "yup";
// import { RadioGroup } from "@app/components/v2/RadioGroup";
import { useCreateIntegration } from "@app/hooks/api";
import { IntegrationSyncBehavior } from "@app/hooks/api/integrations/types";
import {
Button,
@@ -17,54 +34,172 @@ import {
useGetIntegrationAuthApps,
useGetIntegrationAuthById
} from "../../../hooks/api/integrationAuth";
import { useGetWorkspaceById } from "../../../hooks/api/workspace";
import {
// useCreateWsEnvironment,
useGetWorkspaceById
} from "../../../hooks/api/workspace";
const initialSyncBehaviors = [
{ label: "No Import - Overwrite all values in Heroku", value: IntegrationSyncBehavior.OVERWRITE_TARGET },
{ label: "Import - Prefer values from Heroku", value: IntegrationSyncBehavior.PREFER_TARGET },
{ label: "Import - Prefer values from Infisical", value: IntegrationSyncBehavior.PREFER_SOURCE }
];
const schema = yup.object({
selectedSourceEnvironment: yup.string().required("Source environment is required"),
secretPath: yup.string().required("Secret path is required"),
targetApp: yup.string().required("Heroku app is required"),
initialSyncBehavior: yup
.string()
.oneOf(initialSyncBehaviors.map((b) => b.value), "Invalid initial sync behavior")
.required("Initial sync behavior is required")
});
type FormData = yup.InferType<typeof schema>;
export default function HerokuCreateIntegrationPage() {
const router = useRouter();
const { control, handleSubmit, setValue, watch } = useForm<FormData>({
resolver: yupResolver(schema),
defaultValues: {
secretPath: "/",
initialSyncBehavior: IntegrationSyncBehavior.PREFER_SOURCE
}
});
const selectedSourceEnvironment = watch("selectedSourceEnvironment");
const { mutateAsync } = useCreateIntegration();
// const { mutateAsync: mutateAsyncEnv } = useCreateWsEnvironment();
const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]);
const { data: workspace } = useGetWorkspaceById(localStorage.getItem("projectData.id") ?? "");
const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? "");
const { data: integrationAuthApps } = useGetIntegrationAuthApps({
const { data: integrationAuthApps, isLoading: isIntegrationAuthAppsLoading } = useGetIntegrationAuthApps({
integrationAuthId: (integrationAuthId as string) ?? ""
});
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState("");
const [targetApp, setTargetApp] = useState("");
const [secretPath, setSecretPath] = useState("/");
// const { data: integrationAuthPipelineCouplings } = useGetIntegrationAuthHerokuPipelines({
// integrationAuthId: (integrationAuthId as string) ?? ""
// });
// const [uniquePipelines, setUniquePipelines] = useState<Pipeline[]>();
// const [selectedPipeline, setSelectedPipeline] = useState("");
// const [selectedPipelineApps, setSelectedPipelineApps] = useState<App[]>();
// const [integrationType, setIntegrationType] = useState("App");
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
if (workspace) {
setSelectedSourceEnvironment(workspace.environments[0].slug);
setValue("selectedSourceEnvironment", workspace.environments[0].slug);
}
}, [workspace]);
// useEffect(() => {
// if (integrationAuthPipelineCouplings) {
// const uniquePipelinesConst = Array.from(
// new Set(
// integrationAuthPipelineCouplings
// .map(({ pipeline: { pipelineId, name } }) => ({
// name,
// pipelineId
// }))
// .map((obj) => JSON.stringify(obj))
// )).map((str) => JSON.parse(str)) as { pipelineId: string; name: string }[]
// [... (new Set())]
// setUniquePipelines(uniquePipelinesConst);
// if (uniquePipelinesConst) {
// if (uniquePipelinesConst!.length > 0) {
// setSelectedPipeline(uniquePipelinesConst![0].name);
// } else {
// setSelectedPipeline("none");
// }
// }
// }
// }, [integrationAuthPipelineCouplings]);
// useEffect(() => {
// if (integrationAuthPipelineCouplings) {
// setSelectedPipelineApps(integrationAuthApps?.filter(app => integrationAuthPipelineCouplings
// .filter((pipelineCoupling) => pipelineCoupling.pipeline.name === selectedPipeline)
// .map(coupling => coupling.app.appId).includes(String(app.appId))))
// }
// }, [selectedPipeline]);
useEffect(() => {
if (integrationAuthApps) {
if (integrationAuthApps.length > 0) {
setTargetApp(integrationAuthApps[0].name);
setValue("targetApp", integrationAuthApps[0].name);
} else {
setTargetApp("none");
setValue("targetApp", "none");
}
}
}, [integrationAuthApps]);
const handleButtonClick = async () => {
try {
setIsLoading(true);
// const handleButtonClick = async () => {
// try {
// setIsLoading(true);
// if (!integrationAuth?.id) return;
// if (integrationType === "App") {
// await mutateAsync({
// integrationAuthId: integrationAuth?.id,
// isActive: true,
// app: targetApp,
// sourceEnvironment: selectedSourceEnvironment,
// secretPath
// });
// } else if (integrationType === "Pipeline") {
// selectedPipelineApps?.map(async (app, index) => {
// setTimeout(async () => {
// await mutateAsyncEnv({
// workspaceId: String(localStorage.getItem("projectData.id")),
// name: app.name,
// slug: app.name.toLowerCase().replaceAll(" ", "-")
// });
// await mutateAsync({
// integrationAuthId: integrationAuth?.id,
// isActive: true,
// app: app.name,
// sourceEnvironment: app.name.toLowerCase().replaceAll(" ", "-"),
// secretPath
// })
// }, 1000*index)
// })
// }
// setIsLoading(false);
// router.push(`/integrations/${localStorage.getItem("projectData.id")}`);
// } catch (err) {
// console.error(err);
// }
// };
const onFormSubmit = async ({
secretPath,
targetApp,
initialSyncBehavior,
}: FormData) => {
try {
if (!integrationAuth?.id) return;
setIsLoading(true);
await mutateAsync({
integrationAuthId: integrationAuth?.id,
isActive: true,
app: targetApp,
sourceEnvironment: selectedSourceEnvironment,
secretPath
secretPath,
metadata: {
initialSyncBehavior
}
});
setIsLoading(false);
@@ -72,75 +207,203 @@ export default function HerokuCreateIntegrationPage() {
} catch (err) {
console.error(err);
}
};
}
return integrationAuth &&
workspace &&
selectedSourceEnvironment &&
integrationAuthApps &&
targetApp ? (
<div className="flex h-full w-full items-center justify-center">
<Card className="max-w-md rounded-md p-8">
<CardTitle className="text-center">Heroku Integration</CardTitle>
<FormControl label="Project Environment" className="mt-4">
<Select
value={selectedSourceEnvironment}
onValueChange={(val) => setSelectedSourceEnvironment(val)}
className="w-full border border-mineshaft-500"
>
{workspace?.environments.map((sourceEnvironment) => (
<SelectItem
value={sourceEnvironment.slug}
key={`source-environment-${sourceEnvironment.slug}`}
integrationAuthApps ? (
<div className="flex flex-col h-full w-full items-center justify-center">
<Head>
<title>Set Up Heroku Integration</title>
<link rel="icon" href="/infisical.ico" />
</Head>
<Card className="max-w-lg rounded-md border border-mineshaft-600 p-0">
<CardTitle
className="px-6 text-left text-xl"
subTitle="Select which environment or folder in Infisical you want to sync to Heroku's environment variables."
>
<div className="flex flex-row items-center">
<div className="flex items-center">
<Image
src="/images/integrations/Heroku.png"
height={30}
width={30}
alt="Heroku logo"
/>
</div>
<span className="ml-2">Heroku Integration </span>
<Link href="https://infisical.com/docs/integrations/cloud/heroku" passHref>
<a target="_blank" rel="noopener noreferrer">
<div className="ml-2 mb-1 inline-block cursor-default rounded-md bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
Docs
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="ml-1.5 mb-[0.07rem] text-xxs"
/>
</div>
</a>
</Link>
</div>
</CardTitle>
<form onSubmit={handleSubmit(onFormSubmit)} className="px-6">
<Controller
control={control}
name="selectedSourceEnvironment"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Project Environment"
errorText={error?.message}
isError={Boolean(error)}
>
{sourceEnvironment.name}
</SelectItem>
))}
</Select>
</FormControl>
<FormControl label="Secrets Path">
<Input
value={secretPath}
onChange={(evt) => setSecretPath(evt.target.value)}
placeholder="Provide a path, default is /"
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
{workspace?.environments.map((sourceEnvironment) => (
<SelectItem
value={sourceEnvironment.slug}
key={`source-environment-${sourceEnvironment.slug}`}
>
{sourceEnvironment.name}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
</FormControl>
<FormControl label="Heroku App" className="mt-4">
<Select
value={targetApp}
onValueChange={(val) => setTargetApp(val)}
className="w-full border border-mineshaft-500"
<Controller
control={control}
defaultValue=""
name="secretPath"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Secrets Path"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="/" />
</FormControl>
)}
/>
<Controller
control={control}
name="targetApp"
render={({ field: { onChange, ...field }, fieldState: { error } }) => {
return (
<FormControl
label="Heroku App"
errorText={error?.message}
isError={Boolean(error)}
>
<Select
{...field}
onValueChange={(e) => {
if (e === "") return;
onChange(e);
}}
className="w-full"
>
{integrationAuthApps.length > 0 ? (
integrationAuthApps.map((integrationAuthApp) => (
<SelectItem
value={String(integrationAuthApp.name as string)}
key={`target-app-${String(integrationAuthApp.appId)}`}
>
{integrationAuthApp.name}
</SelectItem>
))
) : (
<SelectItem value="none" key="target-app-none">
No apps found
</SelectItem>
)}
</Select>
</FormControl>
);
}}
/>
<Controller
control={control}
name="initialSyncBehavior"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Initial Sync Behavior"
errorText={error?.message}
isError={Boolean(error)}
>
<Select {...field} onValueChange={(e) => onChange(e)} className="w-full">
{initialSyncBehaviors.map((b) => {
return (
<SelectItem value={b.value} key={`sync-behavior-${b.value}`}>
{b.label}
</SelectItem>
);
})}
</Select>
</FormControl>
)}
/>
<Button
colorSchema="primary"
variant="outline_bg"
className="mb-6 mt-2 ml-auto"
size="sm"
type="submit"
isLoading={isLoading}
isDisabled={integrationAuthApps.length === 0}
>
{integrationAuthApps.length > 0 ? (
integrationAuthApps.map((integrationAuthApp) => (
<SelectItem
value={integrationAuthApp.name}
key={`target-app-${integrationAuthApp.name}`}
>
{integrationAuthApp.name}
</SelectItem>
))
) : (
<SelectItem value="none" key="target-app-none">
No apps found
</SelectItem>
)}
</Select>
</FormControl>
<Button
onClick={handleButtonClick}
color="mineshaft"
className="mt-4"
isLoading={isLoading}
isDisabled={integrationAuthApps.length === 0}
>
Create Integration
</Button>
Create Integration
</Button>
</form>
</Card>
{/* {integrationType === "App" && <>
<div className="mt-6 w-full max-w-md border-t border-mineshaft-800" />
<div className="mt-6 flex w-full max-w-lg flex-col rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4">
<div className="flex flex-row items-center">
<FontAwesomeIcon icon={faCircleInfo} className="text-xl text-mineshaft-200" />{" "}
<span className="text-md ml-3 text-mineshaft-100">Pro Tips</span>
</div>
<span className="mt-4 text-sm text-mineshaft-300">
After creating an integration, your secrets will start syncing immediately. This might
cause an unexpected override of current secrets in Heroku with secrets from Infisical.
</span>
</div></>} */}
</div>
) : (
<div />
<div className="flex h-full w-full items-center justify-center">
<Head>
<title>Set Up Vercel Integration</title>
<link rel="icon" href="/infisical.ico" />
</Head>
{isIntegrationAuthAppsLoading ? (
<img
src="/images/loading/loading.gif"
height={70}
width={120}
alt="infisical loading indicator"
/>
) : (
<div className="flex h-max max-w-md flex-col rounded-md border border-mineshaft-600 bg-mineshaft-800 p-6 text-center text-mineshaft-200">
<FontAwesomeIcon icon={faBugs} className="inlineli my-2 text-6xl" />
<p>
Something went wrong. Please contact{" "}
<a
className="inline cursor-pointer text-mineshaft-100 underline decoration-primary-500 underline-offset-4 opacity-80 duration-200 hover:opacity-100"
target="_blank"
rel="noopener noreferrer"
href="mailto:support@infisical.com"
>
support@infisical.com
</a>{" "}
if the issue persists.
</p>
</div>
)}
</div>
);
}

View File

@@ -74,16 +74,6 @@ export default function RailwayCreateIntegrationPage() {
}
}, [targetEnvironments]);
useEffect(() => {
if (targetServices) {
if (targetServices.length > 0) {
setTargetServiceId(targetServices[0].serviceId);
} else {
setTargetServiceId("none");
}
}
}, [targetServices]);
const handleButtonClick = async () => {
try {
setIsLoading(true);
@@ -124,11 +114,14 @@ export default function RailwayCreateIntegrationPage() {
}
};
const filteredTargetServices = targetServices ? [ { name: "", serviceId: "none" }, ...targetServices ] : [ { name: "", serviceId: "none" } ];
return workspace &&
selectedSourceEnvironment &&
integrationAuthApps &&
targetEnvironments &&
targetServices ? (
targetServices &&
filteredTargetServices ? (
<div className="flex h-full w-full items-center justify-center">
<Card className="max-w-md rounded-md p-8">
<CardTitle className="text-center">Railway Integration</CardTitle>
@@ -208,20 +201,14 @@ export default function RailwayCreateIntegrationPage() {
className="w-full border border-mineshaft-500"
isDisabled={targetServices.length === 0}
>
{targetServices.length > 0 ? (
targetServices.map((targetService) => (
{filteredTargetServices.map((targetService) => (
<SelectItem
value={targetService.serviceId as string}
key={`target-service-${targetService.serviceId as string}`}
>
{targetService.name}
</SelectItem>
))
) : (
<SelectItem value="none" key="target-service-none">
No services found
</SelectItem>
)}
))}
</Select>
</FormControl>
<Button

View File

@@ -62,7 +62,7 @@ export const IntegrationsSection = ({
<Link href={`/project/${workspaceId}/settings`} passHref>
<a className="underline underline-offset-2">project settings </a>
</Link>
to re-enable it .
to re-enable it.
</AlertDescription>
</Alert>
</div>
@@ -80,7 +80,7 @@ export const IntegrationsSection = ({
<div className="flex flex-col space-y-4 p-6 pt-0">
{integrations?.map((integration) => (
<div
className="max-w-8xl flex justify-between rounded-md border border-mineshaft-600 bg-mineshaft-800 p-6 pb-2"
className="max-w-8xl flex justify-between rounded-md border border-mineshaft-600 bg-mineshaft-800 p-3 pb-0"
key={`integration-${integration?.id.toString()}`}
>
<div className="flex">

View File

@@ -1,10 +1,11 @@
import { useCallback, useState } from "react";
import { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useRouter } from "next/router";
import { subject } from "@casl/ability";
import { faArrowDown, faArrowUp } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import NavHeader from "@app/components/navigation/NavHeader";
import { PermissionDeniedBanner } from "@app/components/permissions";
import { ContentLoader } from "@app/components/v2";
@@ -47,9 +48,10 @@ const LOADER_TEXT = [
export const SecretMainPage = () => {
const { t } = useTranslation();
const { currentWorkspace } = useWorkspace();
const { currentWorkspace, isLoading: isWorkspaceLoading } = useWorkspace();
const router = useRouter();
const { permission } = useProjectPermission();
const { createNotification } = useNotificationContext();
const [isVisible, setIsVisible] = useState(false);
const [sortDir, setSortDir] = useState<SortDir>(SortDir.ASC);
@@ -75,6 +77,20 @@ export const SecretMainPage = () => {
ProjectPermissionSub.SecretRollback
);
useEffect(() => {
if (
!isWorkspaceLoading &&
!currentWorkspace?.environments.find((env) => env.slug === environment) &&
router.isReady
) {
router.push(`/project/${workspaceId}/secrets/overview`);
createNotification({
text: "No envronment found with given slug",
type: "error"
});
}
}, [isWorkspaceLoading, currentWorkspace, environment, router.isReady]);
const { data: decryptFileKey } = useGetUserWsKey(workspaceId);
// fetch secrets

View File

@@ -291,7 +291,7 @@ export const SecretDetailSidebar = ({
)}
</ProjectPermissionCan>
<DropdownMenuContent align="end" className="z-[100]">
<DropdownMenuLabel>Apply tags to this secrets</DropdownMenuLabel>
<DropdownMenuLabel>Add tags to this secret</DropdownMenuLabel>
{tags.map((tag) => {
const { id: tagId, name, color } = tag;

View File

@@ -325,7 +325,7 @@ export const SecretItem = memo(
)}
</ProjectPermissionCan>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Apply tags to this secrets</DropdownMenuLabel>
<DropdownMenuLabel>Add tags to this secret</DropdownMenuLabel>
{tags.map((tag) => {
const { id: tagId, name, color } = tag;

View File

@@ -3,12 +3,14 @@ import { useTranslation } from "react-i18next";
import Link from "next/link";
import { useRouter } from "next/router";
import { subject } from "@casl/ability";
import { faCheckCircle } from "@fortawesome/free-regular-svg-icons";
import {
faAngleDown,
faArrowDown,
faArrowUp,
faFolderBlank,
faFolderPlus,
faList,
faMagnifyingGlass,
faPlus
} from "@fortawesome/free-solid-svg-icons";
@@ -21,6 +23,8 @@ import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuTrigger,
EmptyState,
IconButton,
@@ -104,6 +108,7 @@ export const SecretOverviewPage = () => {
}, [isWorkspaceLoading, workspaceId, router.isReady]);
const userAvailableEnvs = currentWorkspace?.environments || [];
const [visibleEnvs, setVisisbleEnvs] = useState(userAvailableEnvs);
const {
data: secrets,
@@ -201,6 +206,14 @@ export const SecretOverviewPage = () => {
}
};
const handleEnvSelect = (envId: string) => {
if (visibleEnvs.map((env) => env.id).includes(envId)) {
setVisisbleEnvs(visibleEnvs.filter((env) => env.id !== envId));
} else {
setVisisbleEnvs(visibleEnvs.concat(userAvailableEnvs.filter((env) => env.id === envId)));
}
};
const handleSecretUpdate = async (env: string, key: string, value: string, secretId?: string) => {
try {
await updateSecretV3({
@@ -277,7 +290,7 @@ export const SecretOverviewPage = () => {
}
}
const query: Record<string, string> = { ...router.query, env: slug };
const envIndex = userAvailableEnvs.findIndex((el) => slug === el.slug);
const envIndex = visibleEnvs.findIndex((el) => slug === el.slug);
if (envIndex !== -1) {
router.push({
pathname: "/project/[id]/secrets/[env]",
@@ -377,6 +390,54 @@ export const SecretOverviewPage = () => {
<div className="flex items-center justify-between">
<FolderBreadCrumbs secretPath={secretPath} onResetSearch={handleResetSearch} />
<div className="flex flex-row items-center justify-center space-x-2">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<IconButton
ariaLabel="Environments"
variant="plain"
size="sm"
className="flex h-10 w-11 items-center justify-center overflow-hidden border border-mineshaft-600 bg-mineshaft-800 p-0 hover:border-primary/60 hover:bg-primary/10"
>
<Tooltip content="Choose visible environments" className="mb-2">
<FontAwesomeIcon icon={faList} />
</Tooltip>
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Choose visible environments</DropdownMenuLabel>
{userAvailableEnvs.map((avaiableEnv) => {
const { id: envId, name } = avaiableEnv;
const isEnvSelected = visibleEnvs.map((env) => env.id).includes(envId);
return (
<DropdownMenuItem
onClick={() => handleEnvSelect(envId)}
key={envId}
icon={
isEnvSelected && (
<FontAwesomeIcon className="text-primary" icon={faCheckCircle} />
)
}
iconPos="left"
>
<div className="flex items-center">{name}</div>
</DropdownMenuItem>
);
})}
{/* <DropdownMenuItem className="px-1.5" asChild>
<Button
size="xs"
className="w-full"
colorSchema="primary"
variant="outline_bg"
leftIcon={<FontAwesomeIcon icon={faHockeyPuck} />}
// onClick={onCreateTag}
>
Create an environment
</Button>
</DropdownMenuItem> */}
</DropdownMenuContent>
</DropdownMenu>
<div className="w-80">
<Input
className="h-[2.3rem] bg-mineshaft-800 placeholder-mineshaft-50 duration-200 focus:bg-mineshaft-700/80"
@@ -463,7 +524,7 @@ export const SecretOverviewPage = () => {
</IconButton>
</div>
</Th>
{userAvailableEnvs?.map(({ name, slug }, index) => {
{visibleEnvs?.map(({ name, slug }, index) => {
const envSecKeyCount = getEnvSecretKeyCount(slug);
const missingKeyCount = secKeys.length - envSecKeyCount;
return (
@@ -498,7 +559,7 @@ export const SecretOverviewPage = () => {
<TBody>
{canViewOverviewPage && isTableLoading && (
<TableSkeleton
columns={userAvailableEnvs.length + 1}
columns={visibleEnvs.length + 1}
innerKey="secret-overview-loading"
rows={5}
className="bg-mineshaft-700"
@@ -506,12 +567,12 @@ export const SecretOverviewPage = () => {
)}
{isTableEmpty && !isTableLoading && (
<Tr>
<Td colSpan={userAvailableEnvs.length + 1}>
<Td colSpan={visibleEnvs.length + 1}>
<EmptyState title="Let's add some secrets" icon={faFolderBlank} iconSize="3x">
<Link
href={{
pathname: "/project/[id]/secrets/[env]",
query: { id: workspaceId, env: userAvailableEnvs?.[0]?.slug }
query: { id: workspaceId, env: visibleEnvs?.[0]?.slug }
}}
>
<Button
@@ -520,7 +581,7 @@ export const SecretOverviewPage = () => {
colorSchema="primary"
size="md"
>
Go to {userAvailableEnvs?.[0]?.name}
Go to {visibleEnvs?.[0]?.name}
</Button>
</Link>
</EmptyState>
@@ -532,13 +593,13 @@ export const SecretOverviewPage = () => {
<SecretOverviewFolderRow
folderName={folderName}
isFolderPresentInEnv={isFolderPresentInEnv}
environments={userAvailableEnvs}
environments={visibleEnvs}
key={`overview-${folderName}-${index + 1}`}
onClick={handleFolderClick}
/>
))}
{!isTableLoading &&
(userAvailableEnvs?.length > 0 ? (
(visibleEnvs?.length > 0 ? (
filteredSecretNames.map((key, index) => (
<SecretOverviewTableRow
secretPath={secretPath}
@@ -546,7 +607,7 @@ export const SecretOverviewPage = () => {
onSecretDelete={handleSecretDelete}
onSecretUpdate={handleSecretUpdate}
key={`overview-${key}-${index + 1}`}
environments={userAvailableEnvs}
environments={visibleEnvs}
secretKey={key}
getSecretByKey={getSecretByKey}
expandableColWidth={expandableTableWidth}
@@ -564,7 +625,7 @@ export const SecretOverviewPage = () => {
style={{ height: "45px" }}
/>
</Td>
{userAvailableEnvs.map(({ name, slug }) => (
{visibleEnvs?.map(({ name, slug }) => (
<Td key={`explore-${name}-btn`} className="border-0 border-mineshaft-600 p-0">
<div className="flex w-full items-center justify-center border-r border-t border-mineshaft-600 px-5 py-2">
<Button

View File

@@ -75,7 +75,7 @@ export const CreateSecretForm = ({
const isEnvironmentsSelected = environmentsSelected.length;
if (!isEnvironmentsSelected) {
createNotification({ type: "error", text: "Select atleast one environment" });
createNotification({ type: "error", text: "Select at least one environment" });
return;
}
@@ -143,8 +143,8 @@ export const CreateSecretForm = ({
<Modal isOpen={isOpen} onOpenChange={onTogglePopUp}>
<ModalContent
className="max-h-[80vh] overflow-y-auto"
title="Add Secrets"
subTitle="Add secrets in all environments at once."
title="Bulk Create & Update"
subTitle="Create & update a secret across many environments"
>
<form onSubmit={handleSubmit(handleFormSubmit)}>
<FormControl label="Key" isError={Boolean(errors?.key)} errorText={errors?.key?.message}>

View File

@@ -15,6 +15,7 @@ import { useToggle } from "@app/hooks";
import { DecryptedSecret } from "@app/hooks/api/secrets/types";
import { SecretEditRow } from "./SecretEditRow";
import SecretRenameRow from "./SecretRenameRow";
type Props = {
secretKey: string;
@@ -105,6 +106,13 @@ export const SecretOverviewTableRow = ({
width: `calc(${expandableColWidth}px - 1rem)`
}}
>
<SecretRenameRow
secretKey={secretKey}
environments={environments}
secretPath={secretPath}
getSecretByKey={getSecretByKey}
/>
<TableContainer>
<table className="secret-table">
<thead>

View File

@@ -0,0 +1,258 @@
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import { subject } from "@casl/ability";
import { faCheck, faClose, faCopy } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { AnimatePresence, motion } from "framer-motion";
import { twMerge } from "tailwind-merge";
import { z } from "zod";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { IconButton, Input, Spinner, Tooltip } from "@app/components/v2";
import {
ProjectPermissionActions,
ProjectPermissionSub,
useProjectPermission,
useWorkspace
} from "@app/context";
import { useToggle } from "@app/hooks";
import { useGetUserWsKey, useUpdateSecretV3 } from "@app/hooks/api";
import { DecryptedSecret } from "@app/hooks/api/types";
import { SecretActionType } from "@app/views/SecretMainPage/components/SecretListView/SecretListView.utils";
type Props = {
secretKey: string;
secretPath: string;
environments: { name: string; slug: string }[];
getSecretByKey: (slug: string, key: string) => DecryptedSecret | undefined;
};
export const formSchema = z.object({
key: z.string().trim().min(1, { message: "Secret key is required" })
});
type TFormSchema = z.infer<typeof formSchema>;
function SecretRenameRow({ environments, getSecretByKey, secretKey, secretPath }: Props) {
const { currentWorkspace } = useWorkspace();
const { permission } = useProjectPermission();
const { createNotification } = useNotificationContext();
const secrets = environments.map((env) => getSecretByKey(env.slug, secretKey));
const isReadOnly = environments.some((env) => {
const environment = env.slug;
const isSecretInEnvReadOnly =
permission.can(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
) &&
permission.cannot(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
if (isSecretInEnvReadOnly) {
return true;
}
return false;
});
const isOverriden = secrets.some(
(secret) =>
secret?.overrideAction === SecretActionType.Created ||
secret?.overrideAction === SecretActionType.Modified
);
const workspaceId = currentWorkspace?.id || "";
const { data: decryptFileKey } = useGetUserWsKey(workspaceId);
const [isSecNameCopied, setIsSecNameCopied] = useToggle(false);
const { mutateAsync: updateSecretV3 } = useUpdateSecretV3();
const {
handleSubmit,
control,
reset,
trigger,
getValues,
formState: { isDirty, isSubmitting, errors }
} = useForm<TFormSchema>({
defaultValues: { key: secretKey },
values: { key: secretKey },
resolver: zodResolver(formSchema)
});
useEffect(() => {
let timer: NodeJS.Timeout;
if (isSecNameCopied) {
timer = setTimeout(() => setIsSecNameCopied.off(), 2000);
}
return () => clearTimeout(timer);
}, [isSecNameCopied]);
const handleFormSubmit = async (data: TFormSchema) => {
if (!data.key) {
createNotification({
type: "error",
text: "Secret name cannot be empty"
});
return;
}
const promises = secrets
.filter((secret) => !!secret)
.map((secret) => {
if (!secret) return null;
return updateSecretV3({
environment: secret?.env,
workspaceId,
secretPath,
secretName: secret.key,
secretId: secret.id,
secretValue: secret.value || "",
type: "shared",
latestFileKey: decryptFileKey!,
tags: secret.tags.map((tag) => tag.id),
secretComment: secret.comment,
secretReminderRepeatDays: secret.reminderRepeatDays,
secretReminderNote: secret.reminderNote,
skipMultilineEncoding: secret.skipMultilineEncoding,
newSecretName: data.key
});
});
await Promise.all(promises)
.then(() => {
createNotification({
type: "success",
text: "Successfully renamed the secret"
});
})
.catch(() => {
createNotification({
type: "error",
text: "Error renaming the secret"
});
});
};
const copyTokenToClipboard = () => {
const [key] = getValues(["key"]);
navigator.clipboard.writeText(key as string);
setIsSecNameCopied.on();
};
return (
<form
onSubmit={handleSubmit(handleFormSubmit)}
className="secret-table relative mb-2 flex w-full flex-row items-center justify-between overflow-hidden rounded-lg border border-solid border-mineshaft-700 bg-mineshaft-800 font-inter"
>
<div className="flex h-11 flex-1 flex-shrink-0 items-center">
<span className="flex h-full min-w-[11rem] items-center justify-start border-r-2 border-mineshaft-600 px-4">
Key
</span>
<Controller
name="key"
control={control}
render={({ field, fieldState: { error } }) => (
<Input
autoComplete="off"
isReadOnly={isReadOnly}
autoCapitalization={currentWorkspace?.autoCapitalization}
variant="plain"
isDisabled={isOverriden}
placeholder={error?.message}
onKeyUp={() => trigger("key")}
isError={Boolean(error)}
{...field}
className="w-full px-2 placeholder:text-red-500 focus:text-bunker-100 focus:ring-transparent"
/>
)}
/>
</div>
{isReadOnly || isOverriden ? (
<span className="mr-5 rounded-md bg-mineshaft-500 px-2">Read Only</span>
) : (
<div className="group flex w-20 items-center justify-center border-l border-mineshaft-500 py-1">
<AnimatePresence exitBeforeEnter>
{!isDirty ? (
<motion.div
key="options"
className="flex flex-shrink-0 items-center space-x-4 px-3"
initial={{ x: 0, opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
exit={{ x: 10, opacity: 0 }}
>
<Tooltip content="Copy secret">
<IconButton
ariaLabel="copy-value"
variant="plain"
size="sm"
className="p-0 opacity-0 group-hover:opacity-100"
onClick={copyTokenToClipboard}
>
<FontAwesomeIcon icon={isSecNameCopied ? faCheck : faCopy} />
</IconButton>
</Tooltip>
</motion.div>
) : (
<motion.div
key="options-save"
className="flex flex-shrink-0 items-center space-x-4 px-3"
initial={{ x: -10, opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
exit={{ x: -10, opacity: 0 }}
>
<Tooltip content={errors.key ? errors.key.message : "Save"}>
<IconButton
ariaLabel="more"
variant="plain"
type="submit"
size="md"
className={twMerge(
"p-0 text-primary opacity-0 group-hover:opacity-100",
isDirty && "opacity-100"
)}
isDisabled={isSubmitting || Boolean(errors.key)}
>
{isSubmitting ? (
<Spinner className="m-0 h-4 w-4 p-0" />
) : (
<FontAwesomeIcon
icon={faCheck}
size="lg"
className={twMerge("text-primary", errors.key && "text-mineshaft-400")}
/>
)}
</IconButton>
</Tooltip>
<Tooltip content="Cancel">
<IconButton
ariaLabel="more"
variant="plain"
size="md"
className={twMerge(
"p-0 opacity-0 group-hover:opacity-100",
isDirty && "opacity-100"
)}
onClick={() => reset()}
isDisabled={isSubmitting}
>
<FontAwesomeIcon icon={faClose} size="lg" />
</IconButton>
</Tooltip>
</motion.div>
)}
</AnimatePresence>
</div>
)}
</form>
);
}
export default SecretRenameRow;

View File

@@ -0,0 +1,52 @@
import { useCallback } from "react";
import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { Button } from "@app/components/v2";
import { useToggle } from "@app/hooks";
type Props = {
value: string;
hoverText: string;
notificationText: string;
children: React.ReactNode;
};
export const CopyButton = ({ value, children, hoverText, notificationText }: Props) => {
const [isProjectIdCopied, setIsProjectIdCopied] = useToggle(false);
const { createNotification } = useNotificationContext();
const copyToClipboard = useCallback(() => {
if (isProjectIdCopied) {
return;
}
setIsProjectIdCopied.on();
navigator.clipboard.writeText(value);
createNotification({
text: notificationText,
type: "success"
});
const timer = setTimeout(() => setIsProjectIdCopied.off(), 2000);
// eslint-disable-next-line consistent-return
return () => clearTimeout(timer);
}, [isProjectIdCopied]);
return (
<Button
colorSchema="secondary"
className="group relative"
leftIcon={<FontAwesomeIcon icon={isProjectIdCopied ? faCheck : faCopy} />}
onClick={copyToClipboard}
>
{children}
<span className="absolute -left-8 -top-20 hidden translate-y-full justify-center rounded-md bg-bunker-800 py-2 px-3 text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
{hoverText}
</span>
</Button>
);
};

View File

@@ -1,7 +1,5 @@
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
@@ -9,9 +7,10 @@ import { useNotificationContext } from "@app/components/context/Notifications/No
import { ProjectPermissionCan } from "@app/components/permissions";
import { Button, FormControl, Input } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { useToggle } from "@app/hooks";
import { useRenameWorkspace } from "@app/hooks/api";
import { CopyButton } from "./CopyButton";
const formSchema = yup.object({
name: yup.string().required().label("Project Name")
});
@@ -22,7 +21,6 @@ export const ProjectNameChangeSection = () => {
const { createNotification } = useNotificationContext();
const { currentWorkspace } = useWorkspace();
const { mutateAsync, isLoading } = useRenameWorkspace();
const [isProjectIdCopied, setIsProjectIdCopied] = useToggle(false);
const { handleSubmit, control, reset } = useForm<FormData>({ resolver: yupResolver(formSchema) });
@@ -34,16 +32,6 @@ export const ProjectNameChangeSection = () => {
}
}, [currentWorkspace]);
useEffect(() => {
let timer: NodeJS.Timeout;
if (isProjectIdCopied) {
timer = setTimeout(() => setIsProjectIdCopied.off(), 2000);
}
return () => clearTimeout(timer);
}, [setIsProjectIdCopied]);
const onFormSubmit = async ({ name }: FormData) => {
try {
if (!currentWorkspace?.id) return;
@@ -66,35 +54,28 @@ export const ProjectNameChangeSection = () => {
}
};
const copyProjectIdToClipboard = () => {
navigator.clipboard.writeText(currentWorkspace?.id || "");
setIsProjectIdCopied.on();
createNotification({
text: "Copied Project ID to clipboard",
type: "success"
});
}
return (
<form
onSubmit={handleSubmit(onFormSubmit)}
className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"
>
<div className="flex justify-betweens">
<h2 className="text-xl font-semibold flex-1 text-mineshaft-100 mb-8">Project Name</h2>
<div>
<Button
colorSchema="secondary"
className="group relative"
leftIcon={<FontAwesomeIcon icon={isProjectIdCopied ? faCheck : faCopy} />}
onClick={copyProjectIdToClipboard}
<div className="justify-betweens flex">
<h2 className="mb-8 flex-1 text-xl font-semibold text-mineshaft-100">Project Name</h2>
<div className="space-x-2">
<CopyButton
value={currentWorkspace?.slug || ""}
hoverText="Click to project slug"
notificationText="Copied project slug to clipboard"
>
Copy Project Slug
</CopyButton>
<CopyButton
value={currentWorkspace?.id || ""}
hoverText="Click to project ID"
notificationText="Copied project ID to clipboard"
>
Copy Project ID
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
Click to copy
</span>
</Button>
</CopyButton>
</div>
</div>

603
package-lock.json generated
View File

@@ -6,12 +6,26 @@
"": {
"name": "infisical",
"license": "ISC",
"dependencies": {
"@radix-ui/react-radio-group": "^1.1.3"
},
"devDependencies": {
"@types/uuid": "^9.0.7",
"eslint": "^8.29.0",
"husky": "^8.0.3"
}
},
"node_modules/@babel/runtime": {
"version": "7.23.9",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.9.tgz",
"integrity": "sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==",
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@eslint/eslintrc": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.3.tgz",
@@ -103,6 +117,324 @@
"node": ">= 8"
}
},
"node_modules/@radix-ui/primitive": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.1.tgz",
"integrity": "sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==",
"dependencies": {
"@babel/runtime": "^7.13.10"
}
},
"node_modules/@radix-ui/react-collection": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.0.3.tgz",
"integrity": "sha512-3SzW+0PW7yBBoQlT8wNcGtaxaD0XSu0uLUFgrtHY08Acx05TaHaOmVLR73c0j/cqpDy53KBMO7s0dx2wmOIDIA==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-compose-refs": "1.0.1",
"@radix-ui/react-context": "1.0.1",
"@radix-ui/react-primitive": "1.0.3",
"@radix-ui/react-slot": "1.0.2"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-compose-refs": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz",
"integrity": "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==",
"dependencies": {
"@babel/runtime": "^7.13.10"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-context": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.0.1.tgz",
"integrity": "sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==",
"dependencies": {
"@babel/runtime": "^7.13.10"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-direction": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.0.1.tgz",
"integrity": "sha512-RXcvnXgyvYvBEOhCBuddKecVkoMiI10Jcm5cTI7abJRAHYfFxeu+FBQs/DvdxSYucxR5mna0dNsL6QFlds5TMA==",
"dependencies": {
"@babel/runtime": "^7.13.10"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-id": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.1.tgz",
"integrity": "sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-use-layout-effect": "1.0.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-presence": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.0.1.tgz",
"integrity": "sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-compose-refs": "1.0.1",
"@radix-ui/react-use-layout-effect": "1.0.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-primitive": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz",
"integrity": "sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-slot": "1.0.2"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-radio-group": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.1.3.tgz",
"integrity": "sha512-x+yELayyefNeKeTx4fjK6j99Fs6c4qKm3aY38G3swQVTN6xMpsrbigC0uHs2L//g8q4qR7qOcww8430jJmi2ag==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/primitive": "1.0.1",
"@radix-ui/react-compose-refs": "1.0.1",
"@radix-ui/react-context": "1.0.1",
"@radix-ui/react-direction": "1.0.1",
"@radix-ui/react-presence": "1.0.1",
"@radix-ui/react-primitive": "1.0.3",
"@radix-ui/react-roving-focus": "1.0.4",
"@radix-ui/react-use-controllable-state": "1.0.1",
"@radix-ui/react-use-previous": "1.0.1",
"@radix-ui/react-use-size": "1.0.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-roving-focus": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.4.tgz",
"integrity": "sha512-2mUg5Mgcu001VkGy+FfzZyzbmuUWzgWkj3rvv4yu+mLw03+mTzbxZHvfcGyFp2b8EkQeMkpRQ5FiA2Vr2O6TeQ==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/primitive": "1.0.1",
"@radix-ui/react-collection": "1.0.3",
"@radix-ui/react-compose-refs": "1.0.1",
"@radix-ui/react-context": "1.0.1",
"@radix-ui/react-direction": "1.0.1",
"@radix-ui/react-id": "1.0.1",
"@radix-ui/react-primitive": "1.0.3",
"@radix-ui/react-use-callback-ref": "1.0.1",
"@radix-ui/react-use-controllable-state": "1.0.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-slot": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz",
"integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-compose-refs": "1.0.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-callback-ref": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz",
"integrity": "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==",
"dependencies": {
"@babel/runtime": "^7.13.10"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-controllable-state": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.1.tgz",
"integrity": "sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-use-callback-ref": "1.0.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-layout-effect": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz",
"integrity": "sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==",
"dependencies": {
"@babel/runtime": "^7.13.10"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-previous": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.0.1.tgz",
"integrity": "sha512-cV5La9DPwiQ7S0gf/0qiD6YgNqM5Fk97Kdrlc5yBcrF3jyEZQwm7vYFqMo4IfeHgJXsRaMvLABFtd0OVEmZhDw==",
"dependencies": {
"@babel/runtime": "^7.13.10"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-size": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.0.1.tgz",
"integrity": "sha512-ibay+VqrgcaI6veAojjofPATwledXiSmX+C0KrBk/xgpX9rBzPV3OsfwlhQdUOFbh+LKQorLYT+xTXW9V8yd0g==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-use-layout-effect": "1.0.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@types/uuid": {
"version": "9.0.7",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.7.tgz",
@@ -719,6 +1051,12 @@
"url": "https://opencollective.com/js-sdsl"
}
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"peer": true
},
"node_modules/js-yaml": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
@@ -777,6 +1115,18 @@
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
"dev": true
},
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"peer": true,
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
},
"bin": {
"loose-envify": "cli.js"
}
},
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@@ -934,6 +1284,36 @@
}
]
},
"node_modules/react": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
"integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-dom": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
"integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.0"
},
"peerDependencies": {
"react": "^18.2.0"
}
},
"node_modules/regenerator-runtime": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
},
"node_modules/regexpp": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz",
@@ -1003,6 +1383,15 @@
"queue-microtask": "^1.2.2"
}
},
"node_modules/scheduler": {
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz",
"integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0"
}
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -1143,6 +1532,14 @@
}
},
"dependencies": {
"@babel/runtime": {
"version": "7.23.9",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.9.tgz",
"integrity": "sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==",
"requires": {
"regenerator-runtime": "^0.14.0"
}
},
"@eslint/eslintrc": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.3.tgz",
@@ -1209,6 +1606,164 @@
"fastq": "^1.6.0"
}
},
"@radix-ui/primitive": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.1.tgz",
"integrity": "sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==",
"requires": {
"@babel/runtime": "^7.13.10"
}
},
"@radix-ui/react-collection": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.0.3.tgz",
"integrity": "sha512-3SzW+0PW7yBBoQlT8wNcGtaxaD0XSu0uLUFgrtHY08Acx05TaHaOmVLR73c0j/cqpDy53KBMO7s0dx2wmOIDIA==",
"requires": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-compose-refs": "1.0.1",
"@radix-ui/react-context": "1.0.1",
"@radix-ui/react-primitive": "1.0.3",
"@radix-ui/react-slot": "1.0.2"
}
},
"@radix-ui/react-compose-refs": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz",
"integrity": "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==",
"requires": {
"@babel/runtime": "^7.13.10"
}
},
"@radix-ui/react-context": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.0.1.tgz",
"integrity": "sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==",
"requires": {
"@babel/runtime": "^7.13.10"
}
},
"@radix-ui/react-direction": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.0.1.tgz",
"integrity": "sha512-RXcvnXgyvYvBEOhCBuddKecVkoMiI10Jcm5cTI7abJRAHYfFxeu+FBQs/DvdxSYucxR5mna0dNsL6QFlds5TMA==",
"requires": {
"@babel/runtime": "^7.13.10"
}
},
"@radix-ui/react-id": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.1.tgz",
"integrity": "sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ==",
"requires": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-use-layout-effect": "1.0.1"
}
},
"@radix-ui/react-presence": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.0.1.tgz",
"integrity": "sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg==",
"requires": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-compose-refs": "1.0.1",
"@radix-ui/react-use-layout-effect": "1.0.1"
}
},
"@radix-ui/react-primitive": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz",
"integrity": "sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==",
"requires": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-slot": "1.0.2"
}
},
"@radix-ui/react-radio-group": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.1.3.tgz",
"integrity": "sha512-x+yELayyefNeKeTx4fjK6j99Fs6c4qKm3aY38G3swQVTN6xMpsrbigC0uHs2L//g8q4qR7qOcww8430jJmi2ag==",
"requires": {
"@babel/runtime": "^7.13.10",
"@radix-ui/primitive": "1.0.1",
"@radix-ui/react-compose-refs": "1.0.1",
"@radix-ui/react-context": "1.0.1",
"@radix-ui/react-direction": "1.0.1",
"@radix-ui/react-presence": "1.0.1",
"@radix-ui/react-primitive": "1.0.3",
"@radix-ui/react-roving-focus": "1.0.4",
"@radix-ui/react-use-controllable-state": "1.0.1",
"@radix-ui/react-use-previous": "1.0.1",
"@radix-ui/react-use-size": "1.0.1"
}
},
"@radix-ui/react-roving-focus": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.4.tgz",
"integrity": "sha512-2mUg5Mgcu001VkGy+FfzZyzbmuUWzgWkj3rvv4yu+mLw03+mTzbxZHvfcGyFp2b8EkQeMkpRQ5FiA2Vr2O6TeQ==",
"requires": {
"@babel/runtime": "^7.13.10",
"@radix-ui/primitive": "1.0.1",
"@radix-ui/react-collection": "1.0.3",
"@radix-ui/react-compose-refs": "1.0.1",
"@radix-ui/react-context": "1.0.1",
"@radix-ui/react-direction": "1.0.1",
"@radix-ui/react-id": "1.0.1",
"@radix-ui/react-primitive": "1.0.3",
"@radix-ui/react-use-callback-ref": "1.0.1",
"@radix-ui/react-use-controllable-state": "1.0.1"
}
},
"@radix-ui/react-slot": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz",
"integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==",
"requires": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-compose-refs": "1.0.1"
}
},
"@radix-ui/react-use-callback-ref": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz",
"integrity": "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==",
"requires": {
"@babel/runtime": "^7.13.10"
}
},
"@radix-ui/react-use-controllable-state": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.1.tgz",
"integrity": "sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==",
"requires": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-use-callback-ref": "1.0.1"
}
},
"@radix-ui/react-use-layout-effect": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz",
"integrity": "sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==",
"requires": {
"@babel/runtime": "^7.13.10"
}
},
"@radix-ui/react-use-previous": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.0.1.tgz",
"integrity": "sha512-cV5La9DPwiQ7S0gf/0qiD6YgNqM5Fk97Kdrlc5yBcrF3jyEZQwm7vYFqMo4IfeHgJXsRaMvLABFtd0OVEmZhDw==",
"requires": {
"@babel/runtime": "^7.13.10"
}
},
"@radix-ui/react-use-size": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.0.1.tgz",
"integrity": "sha512-ibay+VqrgcaI6veAojjofPATwledXiSmX+C0KrBk/xgpX9rBzPV3OsfwlhQdUOFbh+LKQorLYT+xTXW9V8yd0g==",
"requires": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-use-layout-effect": "1.0.1"
}
},
"@types/uuid": {
"version": "9.0.7",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.7.tgz",
@@ -1665,6 +2220,12 @@
"integrity": "sha512-dyBIzQBDkCqCu+0upx25Y2jGdbTGxE9fshMsCdK0ViOongpV+n5tXRcZY9v7CaVQ79AGS9KA1KHtojxiM7aXSQ==",
"dev": true
},
"js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"peer": true
},
"js-yaml": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
@@ -1711,6 +2272,15 @@
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
"dev": true
},
"loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"peer": true,
"requires": {
"js-tokens": "^3.0.0 || ^4.0.0"
}
},
"minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@@ -1818,6 +2388,30 @@
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
"dev": true
},
"react": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
"integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==",
"peer": true,
"requires": {
"loose-envify": "^1.1.0"
}
},
"react-dom": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
"integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==",
"peer": true,
"requires": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.0"
}
},
"regenerator-runtime": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
},
"regexpp": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz",
@@ -1854,6 +2448,15 @@
"queue-microtask": "^1.2.2"
}
},
"scheduler": {
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz",
"integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==",
"peer": true,
"requires": {
"loose-envify": "^1.1.0"
}
},
"shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",

View File

@@ -22,5 +22,8 @@
"@types/uuid": "^9.0.7",
"eslint": "^8.29.0",
"husky": "^8.0.3"
},
"dependencies": {
"@radix-ui/react-radio-group": "^1.1.3"
}
}