Fix merge conflicts

This commit is contained in:
Tuan Dang
2023-06-29 17:53:33 +07:00
38 changed files with 5718 additions and 4180 deletions

View File

@@ -53,7 +53,7 @@ export const getServiceTokenData = async (req: Request, res: Response) => {
req.authData.authPayload._id
)
.select("+encryptedKey +iv +tag")
.populate("user");
.populate("user").lean();
return res.status(200).json(serviceTokenData);
};

View File

@@ -188,7 +188,7 @@ type GetServiceTokenDetailsResponse struct {
Tag string `json:"tag"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
V int `json:"__v"`
SecretPath string `json:"secretPath"`
}
type GetAccessibleEnvironmentsRequest struct {

View File

@@ -37,6 +37,7 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string) ([]models.Singl
encryptedSecrets, err := api.CallGetSecretsV3(httpClient, api.GetEncryptedSecretsV3Request{
WorkspaceId: serviceTokenDetails.Workspace,
Environment: serviceTokenDetails.Environment,
SecretPath: serviceTokenDetails.SecretPath,
})
if err != nil {

View File

@@ -6,7 +6,14 @@ module.exports = {
'@storybook/addon-essentials',
'@storybook/addon-interactions',
'storybook-dark-mode',
'@storybook/addon-postcss'
{
name: '@storybook/addon-styling',
options: {
postCss: {
implementation: require('postcss')
}
}
}
],
framework: {
name: '@storybook/nextjs',

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@
"start": "next start",
"start:docker": "next build && next start",
"lint": "eslint --ext js,ts,tsx ./src",
"lint-and-fix": "eslint --fix --ext js,ts,tsx ./src",
"lint:fix": "eslint --fix --ext js,ts,tsx ./src",
"type-check": "tsc --project tsconfig.json",
"storybook": "storybook dev -p 6006 -s ./public",
"build-storybook": "storybook build"
@@ -62,8 +62,8 @@
"infisical-node": "^1.0.37",
"jspdf": "^2.5.1",
"jsrp": "^0.2.4",
"lottie-react": "^2.4.0",
"jwt-decode": "^3.1.2",
"lottie-react": "^2.4.0",
"markdown-it": "^13.0.1",
"next": "^12.3.4",
"posthog-js": "^1.58.0",
@@ -91,14 +91,14 @@
"yup": "^0.32.11"
},
"devDependencies": {
"@storybook/addon-essentials": "^7.0.0-beta.30",
"@storybook/addon-interactions": "^7.0.0-beta.30",
"@storybook/addon-links": "^7.0.0-beta.30",
"@storybook/addon-postcss": "^2.0.0",
"@storybook/blocks": "^7.0.0-beta.30",
"@storybook/nextjs": "^7.0.0-beta.30",
"@storybook/react": "^7.0.0-beta.30",
"@storybook/testing-library": "^0.0.13",
"@storybook/addon-essentials": "^7.0.23",
"@storybook/addon-interactions": "^7.0.23",
"@storybook/addon-links": "^7.0.23",
"@storybook/addon-styling": "^1.3.0",
"@storybook/blocks": "^7.0.23",
"@storybook/nextjs": "^7.0.23",
"@storybook/react": "^7.0.23",
"@storybook/testing-library": "^0.2.0",
"@tailwindcss/typography": "^0.5.4",
"@types/jsrp": "^0.2.4",
"@types/node": "18.11.9",
@@ -118,12 +118,12 @@
"eslint-plugin-react": "^7.32.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-simple-import-sort": "^8.0.0",
"eslint-plugin-storybook": "^0.6.10",
"eslint-plugin-storybook": "^0.6.12",
"postcss": "^8.4.14",
"prettier": "^2.8.3",
"prettier-plugin-tailwindcss": "^0.2.2",
"storybook": "^7.0.0-beta.30",
"storybook-dark-mode": "^2.0.5",
"storybook": "^7.0.23",
"storybook-dark-mode": "^3.0.0",
"tailwindcss": "3.2",
"typescript": "^4.9.3"
}

View File

@@ -13,6 +13,7 @@ type Props = {
onChange?: (isOpen: boolean) => void;
deleteKey: string;
title: string;
subTitle?: string;
onDeleteApproved: () => Promise<void>;
};
@@ -22,7 +23,8 @@ export const DeleteActionModal = ({
onChange,
deleteKey,
onDeleteApproved,
title
title,
subTitle = "This action is irreversible!"
}: Props): JSX.Element => {
const [inputData, setInputData] = useState("");
const [isLoading, setIsLoading] = useToggle();
@@ -52,7 +54,7 @@ export const DeleteActionModal = ({
>
<ModalContent
title={title}
subTitle="This action is irreversible!"
subTitle={subTitle}
footerContent={
<div className="flex items-center">
<Button

View File

@@ -11,7 +11,7 @@ export type FormLabelProps = {
};
export const FormLabel = ({ id, label, isRequired }: FormLabelProps) => (
<Label.Root className="mb-1 ml-0.5 block text-sm font-medium text-mineshaft-300" htmlFor={id}>
<Label.Root className="mb-0.5 ml-1 block text-sm font-normal text-mineshaft-400" htmlFor={id}>
{label}
{isRequired && <span className="ml-1 text-red">*</span>}
</Label.Root>

View File

@@ -1,5 +1,6 @@
export {
useGetAuthToken,
useGetCommonPasswords,
useSendMfaToken,
useVerifyMfaToken} from "./queries"
useGetAuthToken,
useGetCommonPasswords,
useSendMfaToken,
useVerifyMfaToken
} from "./queries"

View File

@@ -0,0 +1 @@
export { useGetWorkspaceBot, useUpdateBotActiveStatus } from "./queries";

View File

@@ -0,0 +1,38 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { TBot, TSetBotActiveStatusDto } from "./types";
const queryKeys = {
getBot: (workspaceId: string) => [{ workspaceId }, "bot"] as const
};
const fetchWorkspaceBot = async (workspaceId: string) => {
const { data } = await apiRequest.get<{ bot: TBot }>(`/api/v1/bot/${workspaceId}`);
return data.bot;
};
export const useGetWorkspaceBot = (workspaceId: string) =>
useQuery({
queryKey: queryKeys.getBot(workspaceId),
queryFn: () => fetchWorkspaceBot(workspaceId),
enabled: Boolean(workspaceId)
});
// mutation
export const useUpdateBotActiveStatus = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, TSetBotActiveStatusDto>({
mutationFn: ({ botId, isActive, botKey }) =>
apiRequest.patch(`/api/v1/bot/${botId}/active`, {
isActive,
botKey
}),
onSuccess: (_, { workspaceId }) => {
queryClient.invalidateQueries(queryKeys.getBot(workspaceId));
}
});
};

View File

@@ -0,0 +1,20 @@
export type TBot = {
_id: string;
name: string;
workspace: string;
isActive: boolean;
publicKey: string;
createdAt: string;
updatedAt: string;
__v: number;
};
export type TSetBotActiveStatusDto = {
workspaceId: string;
botId: string;
isActive: boolean;
botKey?: {
encryptedKey: string;
nonce: string;
};
};

View File

@@ -1,5 +1,8 @@
export * from "./auth";
export * from "./bots";
export * from "./incidentContacts";
export * from "./integrationAuth";
export * from "./integrations";
export * from "./keys";
export * from "./organization";
export * from "./secretFolders";

View File

@@ -1,7 +1,9 @@
export {
useGetIntegrationAuthApps,
useGetIntegrationAuthById,
useGetIntegrationAuthRailwayEnvironments,
useGetIntegrationAuthRailwayServices,
useGetIntegrationAuthTeams,
useGetIntegrationAuthVercelBranches} from "./queries";
useDeleteIntegrationAuth,
useGetIntegrationAuthApps,
useGetIntegrationAuthById,
useGetIntegrationAuthRailwayEnvironments,
useGetIntegrationAuthRailwayServices,
useGetIntegrationAuthTeams,
useGetIntegrationAuthVercelBranches
} from "./queries";

View File

@@ -1,204 +1,237 @@
import { useQuery } from "@tanstack/react-query";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import {
App,
Environment,
IntegrationAuth,
Service,
Team
} from "./types";
import { workspaceKeys } from "../workspace/queries";
import { App, Environment, IntegrationAuth, Service, Team } from "./types";
const integrationAuthKeys = {
getIntegrationAuthById: (integrationAuthId: string) => [{ integrationAuthId }, "integrationAuth"] as const,
getIntegrationAuthApps: (integrationAuthId: string, teamId?: string) => [{ integrationAuthId, teamId }, "integrationAuthApps"] as const,
getIntegrationAuthTeams: (integrationAuthId: string) => [{ integrationAuthId }, "integrationAuthTeams"] as const,
getIntegrationAuthVercelBranches: ({
integrationAuthId,
appId,
}: {
integrationAuthId: string;
appId: string;
}) => [{ integrationAuthId, appId }, "integrationAuthVercelBranches"] as const,
getIntegrationAuthRailwayEnvironments: ({
integrationAuthId,
appId
}: {
integrationAuthId: string;
appId: string;
}) => [{ integrationAuthId, appId }, "integrationAuthRailwayEnvironments"] as const,
getIntegrationAuthRailwayServices: ({
integrationAuthId,
appId
}: {
integrationAuthId: string;
appId: string;
}) => [{ integrationAuthId, appId }, "integrationAuthRailwayServices"] as const
}
const fetchIntegrationAuthById = async (integrationAuthId: string) => {
const { data } = await apiRequest.get<{ integrationAuth: IntegrationAuth }>(`/api/v1/integration-auth/${integrationAuthId}`);
return data.integrationAuth;
}
const fetchIntegrationAuthApps = async ({
integrationAuthId,
teamId
}: {
integrationAuthId: string;
teamId?: string;
}) => {
const searchParams = new URLSearchParams(teamId ? { teamId } : undefined);
const { data } = await apiRequest.get<{ apps: App[] }>(
`/api/v1/integration-auth/${integrationAuthId}/apps`,
{ params: searchParams }
);
return data.apps;
}
const fetchIntegrationAuthTeams = async (integrationAuthId: string) => {
const { data } = await apiRequest.get<{ teams: Team[] }>(`/api/v1/integration-auth/${integrationAuthId}/teams`);
return data.teams;
}
const fetchIntegrationAuthVercelBranches = async ({
getIntegrationAuthById: (integrationAuthId: string) =>
[{ integrationAuthId }, "integrationAuth"] as const,
getIntegrationAuthApps: (integrationAuthId: string, teamId?: string) =>
[{ integrationAuthId, teamId }, "integrationAuthApps"] as const,
getIntegrationAuthTeams: (integrationAuthId: string) =>
[{ integrationAuthId }, "integrationAuthTeams"] as const,
getIntegrationAuthVercelBranches: ({
integrationAuthId,
appId
}: {
}: {
integrationAuthId: string;
appId: string;
}) => [{ integrationAuthId, appId }, "integrationAuthVercelBranches"] as const,
getIntegrationAuthRailwayEnvironments: ({
integrationAuthId,
appId
}: {
integrationAuthId: string;
appId: string;
}) => [{ integrationAuthId, appId }, "integrationAuthRailwayEnvironments"] as const,
getIntegrationAuthRailwayServices: ({
integrationAuthId,
appId
}: {
integrationAuthId: string;
appId: string;
}) => [{ integrationAuthId, appId }, "integrationAuthRailwayServices"] as const
};
const fetchIntegrationAuthById = async (integrationAuthId: string) => {
const { data } = await apiRequest.get<{ integrationAuth: IntegrationAuth }>(
`/api/v1/integration-auth/${integrationAuthId}`
);
return data.integrationAuth;
};
const fetchIntegrationAuthApps = async ({
integrationAuthId,
teamId
}: {
integrationAuthId: string;
teamId?: string;
}) => {
const { data: { branches } } = await apiRequest.get<{ branches: string[] }>(`/api/v1/integration-auth/${integrationAuthId}/vercel/branches`, {
params: {
appId
}
});
return branches;
const searchParams = new URLSearchParams(teamId ? { teamId } : undefined);
const { data } = await apiRequest.get<{ apps: App[] }>(
`/api/v1/integration-auth/${integrationAuthId}/apps`,
{ params: searchParams }
);
return data.apps;
};
const fetchIntegrationAuthTeams = async (integrationAuthId: string) => {
const { data } = await apiRequest.get<{ teams: Team[] }>(
`/api/v1/integration-auth/${integrationAuthId}/teams`
);
return data.teams;
};
const fetchIntegrationAuthVercelBranches = async ({
integrationAuthId,
appId
}: {
integrationAuthId: string;
appId: string;
}) => {
const {
data: { branches }
} = await apiRequest.get<{ branches: string[] }>(
`/api/v1/integration-auth/${integrationAuthId}/vercel/branches`,
{
params: {
appId
}
}
);
return branches;
};
const fetchIntegrationAuthRailwayEnvironments = async ({
integrationAuthId,
appId
integrationAuthId,
appId
}: {
integrationAuthId: string;
appId: string;
integrationAuthId: string;
appId: string;
}) => {
const { data: { environments } } = await apiRequest.get<{ environments: Environment[] }>(`/api/v1/integration-auth/${integrationAuthId}/railway/environments`, {
params: {
appId
}
});
return environments;
}
const {
data: { environments }
} = await apiRequest.get<{ environments: Environment[] }>(
`/api/v1/integration-auth/${integrationAuthId}/railway/environments`,
{
params: {
appId
}
}
);
return environments;
};
const fetchIntegrationAuthRailwayServices = async ({
integrationAuthId,
appId
integrationAuthId,
appId
}: {
integrationAuthId: string;
appId: string;
integrationAuthId: string;
appId: string;
}) => {
const { data: { services } } = await apiRequest.get<{ services: Service[] }>(`/api/v1/integration-auth/${integrationAuthId}/railway/services`, {
params: {
appId
}
});
return services;
}
const {
data: { services }
} = await apiRequest.get<{ services: Service[] }>(
`/api/v1/integration-auth/${integrationAuthId}/railway/services`,
{
params: {
appId
}
}
);
return services;
};
export const useGetIntegrationAuthById = (integrationAuthId: string) => {
return useQuery({
queryKey: integrationAuthKeys.getIntegrationAuthById(integrationAuthId),
queryFn: () => fetchIntegrationAuthById(integrationAuthId),
enabled: true
});
}
return useQuery({
queryKey: integrationAuthKeys.getIntegrationAuthById(integrationAuthId),
queryFn: () => fetchIntegrationAuthById(integrationAuthId),
enabled: true
});
};
export const useGetIntegrationAuthApps = ({
integrationAuthId,
teamId
integrationAuthId,
teamId
}: {
integrationAuthId: string;
teamId?: string;
integrationAuthId: string;
teamId?: string;
}) => {
return useQuery({
queryKey: integrationAuthKeys.getIntegrationAuthApps(integrationAuthId, teamId),
queryFn: () => fetchIntegrationAuthApps({
integrationAuthId,
teamId
}),
enabled: true
});
}
return useQuery({
queryKey: integrationAuthKeys.getIntegrationAuthApps(integrationAuthId, teamId),
queryFn: () =>
fetchIntegrationAuthApps({
integrationAuthId,
teamId
}),
enabled: true
});
};
export const useGetIntegrationAuthTeams = (integrationAuthId: string) => {
return useQuery({
queryKey: integrationAuthKeys.getIntegrationAuthTeams(integrationAuthId),
queryFn: () => fetchIntegrationAuthTeams(integrationAuthId),
enabled: true
});
}
return useQuery({
queryKey: integrationAuthKeys.getIntegrationAuthTeams(integrationAuthId),
queryFn: () => fetchIntegrationAuthTeams(integrationAuthId),
enabled: true
});
};
export const useGetIntegrationAuthVercelBranches = ({
integrationAuthId,
appId,
integrationAuthId,
appId
}: {
integrationAuthId: string;
appId: string;
integrationAuthId: string;
appId: string;
}) => {
return useQuery({
queryKey: integrationAuthKeys.getIntegrationAuthVercelBranches({
integrationAuthId,
appId,
}),
queryFn: () => fetchIntegrationAuthVercelBranches({
integrationAuthId,
appId,
}),
enabled: true
});
}
return useQuery({
queryKey: integrationAuthKeys.getIntegrationAuthVercelBranches({
integrationAuthId,
appId
}),
queryFn: () =>
fetchIntegrationAuthVercelBranches({
integrationAuthId,
appId
}),
enabled: true
});
};
export const useGetIntegrationAuthRailwayEnvironments = ({
integrationAuthId,
appId
integrationAuthId,
appId
}: {
integrationAuthId: string;
appId: string;
integrationAuthId: string;
appId: string;
}) => {
return useQuery({
queryKey: integrationAuthKeys.getIntegrationAuthRailwayEnvironments({
integrationAuthId,
appId,
}),
queryFn: () => fetchIntegrationAuthRailwayEnvironments({
integrationAuthId,
appId,
}),
enabled: true
});
}
return useQuery({
queryKey: integrationAuthKeys.getIntegrationAuthRailwayEnvironments({
integrationAuthId,
appId
}),
queryFn: () =>
fetchIntegrationAuthRailwayEnvironments({
integrationAuthId,
appId
}),
enabled: true
});
};
export const useGetIntegrationAuthRailwayServices = ({
integrationAuthId,
appId
integrationAuthId,
appId
}: {
integrationAuthId: string;
appId: string;
integrationAuthId: string;
appId: string;
}) => {
return useQuery({
queryKey: integrationAuthKeys.getIntegrationAuthRailwayServices({
integrationAuthId,
appId,
}),
queryFn: () => fetchIntegrationAuthRailwayServices({
integrationAuthId,
appId,
}),
enabled: true
});
}
return useQuery({
queryKey: integrationAuthKeys.getIntegrationAuthRailwayServices({
integrationAuthId,
appId
}),
queryFn: () =>
fetchIntegrationAuthRailwayServices({
integrationAuthId,
appId
}),
enabled: true
});
};
export const useDeleteIntegrationAuth = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, { id: string; workspaceId: string }>({
mutationFn: ({ id }) => apiRequest.delete(`/api/v1/integration-auth/${id}`),
onSuccess: (_, { workspaceId }) => {
queryClient.invalidateQueries(workspaceKeys.getWorkspaceAuthorization(workspaceId));
queryClient.invalidateQueries(workspaceKeys.getWorkspaceIntegrations(workspaceId));
}
});
};

View File

@@ -1,28 +1,31 @@
export type IntegrationAuth = {
_id: string;
workspace: string;
integration: string;
teamId?: string;
accountId?: string;
}
_id: string;
integration: string;
workspace: string;
__v: number;
createdAt: string;
updatedAt: string;
algorithm: string;
keyEncoding: string;
};
export type App = {
name: string;
appId?: string;
owner?: string;
}
name: string;
appId?: string;
owner?: string;
};
export type Team = {
name: string;
teamId: string;
}
name: string;
teamId: string;
};
export type Environment = {
name: string;
environmentId: string;
}
name: string;
environmentId: string;
};
export type Service = {
name: string;
serviceId: string;
}
name: string;
serviceId: string;
};

View File

@@ -0,0 +1 @@
export { useDeleteIntegration,useGetCloudIntegrations } from "./queries";

View File

@@ -0,0 +1,35 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { workspaceKeys } from "../workspace/queries";
import { TCloudIntegration } from "./types";
export const integrationQueryKeys = {
getIntegrations: () => ["integrations"] as const
};
const fetchIntegrations = async () => {
const { data } = await apiRequest.get<{ integrationOptions: TCloudIntegration[] }>(
"/api/v1/integration-auth/integration-options"
);
return data.integrationOptions;
};
export const useGetCloudIntegrations = () =>
useQuery({
queryKey: integrationQueryKeys.getIntegrations(),
queryFn: () => fetchIntegrations()
});
export const useDeleteIntegration = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, { id: string; workspaceId: string }>({
mutationFn: ({ id }) => apiRequest.delete(`/api/v1/integration/${id}`),
onSuccess: (_, { workspaceId }) => {
queryClient.invalidateQueries(workspaceKeys.getWorkspaceIntegrations(workspaceId));
}
});
};

View File

@@ -0,0 +1,33 @@
export type TCloudIntegration = {
name: string;
slug: string;
image: string;
isAvailable: boolean;
type: string;
clientId: string;
docsLink: string;
clientSlug: string;
};
export type TIntegration = {
_id: string;
workspace: string;
environment: string;
isActive: boolean;
url: any;
app: string;
appId: string;
targetEnvironment: string;
targetEnvironmentId: string;
targetService: string;
targetServiceId: string;
owner: string;
path: string;
region: string;
integration: string;
integrationAuth: string;
secretPath: string;
createdAt: string;
updatedAt: string;
__v: number;
};

View File

@@ -1,5 +1,7 @@
export type { GetAuthTokenAPI } from "./auth/types";
export type { IncidentContact } from "./incidentContacts/types";
export type { IntegrationAuth } from "./integrationAuth/types";
export type { TCloudIntegration, TIntegration } from "./integrations/types";
export type { UserWsKeyPair } from "./keys/types";
export type { Organization } from "./organization/types";
export type { CreateServiceTokenDTO, ServiceToken } from "./serviceTokens/types";

View File

@@ -6,8 +6,10 @@ export {
useGetUserWorkspaceMemberships,
useGetUserWorkspaces,
useGetUserWsEnvironments,
useGetWorkspaceAuthorizations,
useGetWorkspaceById,
useGetWorkspaceIndexStatus,
useGetWorkspaceIntegrations,
useGetWorkspaceSecrets,
useNameWorkspaceSecrets,
useRenameWorkspace,

View File

@@ -2,9 +2,9 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import {
EncryptedSecret
} from "../secrets/types";
import { IntegrationAuth } from "../integrationAuth/types";
import { TIntegration } from "../integrations/types";
import { EncryptedSecret } from "../secrets/types";
import {
CreateEnvironmentDTO,
CreateWorkspaceDTO,
@@ -19,11 +19,14 @@ import {
WorkspaceEnv
} from "./types";
const workspaceKeys = {
export const workspaceKeys = {
getWorkspaceById: (workspaceId: string) => [{ workspaceId }, "workspace"] as const,
getWorkspaceSecrets: (workspaceId: string) => [{ workspaceId }, "workspace-secrets"] as const,
getWorkspaceIndexStatus: (workspaceId: string) => [{ workspaceId}, "workspace-index-status"] as const,
getWorkspaceIndexStatus: (workspaceId: string) =>
[{ workspaceId }, "workspace-index-status"] as const,
getWorkspaceMemberships: (orgId: string) => [{ orgId }, "workspace-memberships"],
getWorkspaceAuthorization: (workspaceId: string) => [{ workspaceId }, "workspace-authorizations"],
getWorkspaceIntegrations: (workspaceId: string) => [{ workspaceId }, "workspace-integrations"],
getAllUserWorkspace: ["workspaces"] as const,
getUserWsEnvironments: (workspaceId: string) => ["workspace-env", { workspaceId }] as const
};
@@ -42,15 +45,17 @@ const fetchWorkspaceIndexStatus = async (workspaceId: string) => {
);
return data;
}
};
const fetchWorkspaceSecrets = async (workspaceId: string) => {
const { data: { secrets } } = await apiRequest.get<{ secrets: EncryptedSecret[] }>(
const {
data: { secrets }
} = await apiRequest.get<{ secrets: EncryptedSecret[] }>(
`/api/v3/workspaces/${workspaceId}/secrets`
);
return secrets;
}
};
const fetchUserWorkspaces = async () => {
const { data } = await apiRequest.get<{ workspaces: Workspace[] }>("/api/v1/workspace");
@@ -63,15 +68,15 @@ export const useGetWorkspaceIndexStatus = (workspaceId: string) => {
queryFn: () => fetchWorkspaceIndexStatus(workspaceId),
enabled: true
});
}
};
export const useGetWorkspaceSecrets = (workspaceId: string) => {
return useQuery({
queryKey: workspaceKeys.getWorkspaceSecrets(workspaceId),
queryFn: () => fetchWorkspaceSecrets(workspaceId),
enabled: true
})
}
});
};
export const useGetWorkspaceById = (workspaceId: string) => {
return useQuery({
@@ -126,7 +131,39 @@ export const useNameWorkspaceSecrets = () => {
queryClient.invalidateQueries(workspaceKeys.getWorkspaceIndexStatus(variables.workspaceId));
}
});
}
};
const fetchWorkspaceAuthorization = async (workspaceId: string) => {
const { data } = await apiRequest.get<{ authorizations: IntegrationAuth[] }>(
`/api/v1/workspace/${workspaceId}/authorizations`
);
return data.authorizations;
};
export const useGetWorkspaceAuthorizations = <TData = IntegrationAuth[],>(
workspaceId: string,
select?: (data: IntegrationAuth[]) => TData
) =>
useQuery({
queryKey: workspaceKeys.getWorkspaceAuthorization(workspaceId),
queryFn: () => fetchWorkspaceAuthorization(workspaceId),
enabled: Boolean(workspaceId),
select
});
const fetchWorkspaceIntegrations = async (workspaceId: string) => {
const { data } = await apiRequest.get<{ integrations: TIntegration[] }>(
`/api/v1/workspace/${workspaceId}/integrations`
);
return data.integrations;
};
export const useGetWorkspaceIntegrations = (workspaceId: string) =>
useQuery({
queryKey: workspaceKeys.getWorkspaceIntegrations(workspaceId),
queryFn: () => fetchWorkspaceIntegrations(workspaceId),
enabled: Boolean(workspaceId)
});
// mutation
export const useCreateWorkspace = () => {

View File

@@ -1,409 +1,18 @@
import crypto from "crypto";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import Head from "next/head";
import { useRouter } from "next/router";
import frameworkIntegrationOptions from "public/json/frameworkIntegrations.json";
import ActivateBotDialog from "@app/components/basic/dialog/ActivateBotDialog";
import CloudIntegrationSection from "@app/components/integrations/CloudIntegrationSection";
import FrameworkIntegrationSection from "@app/components/integrations/FrameworkIntegrationSection";
import IntegrationSection from "@app/components/integrations/IntegrationSection";
import NavHeader from "@app/components/navigation/NavHeader";
import { IntegrationsPage } from "@app/views/IntegrationsPage";
import {
decryptAssymmetric,
encryptAssymmetric
} from "../../components/utilities/cryptography/crypto";
import getBot from "../api/bot/getBot";
import setBotActiveStatus from "../api/bot/setBotActiveStatus";
import deleteIntegration from "../api/integrations/DeleteIntegration";
import getIntegrationOptions from "../api/integrations/GetIntegrationOptions";
import getWorkspaceAuthorizations from "../api/integrations/getWorkspaceAuthorizations";
import getWorkspaceIntegrations from "../api/integrations/getWorkspaceIntegrations";
import getAWorkspace from "../api/workspace/getAWorkspace";
import getLatestFileKey from "../api/workspace/getLatestFileKey";
interface IntegrationAuth {
_id: string;
integration: string;
workspace: string;
createdAt: string;
updatedAt: string;
}
interface Integration {
_id: string;
isActive: boolean;
app: string | null;
appId: string | null;
createdAt: string;
updatedAt: string;
environment: string;
integration: string;
targetEnvironment: string;
workspace: string;
secretPath:string;
integrationAuth: string;
}
interface IntegrationOption {
tenantId?: string;
clientId: string;
clientSlug?: string; // vercel-integration specific
docsLink: string;
image: string;
isAvailable: boolean;
name: string;
slug: string;
type: string;
}
export default function Integrations() {
const [cloudIntegrationOptions, setCloudIntegrationOptions] = useState([]);
const [integrationAuths, setIntegrationAuths] = useState<IntegrationAuth[]>([]);
const [environments, setEnvironments] = useState<
{
name: string;
slug: string;
}[]
>([]);
const [integrations, setIntegrations] = useState<Integration[]>([]);
// TODO: These will have its type when migratiing towards react-query
const [bot, setBot] = useState<any>(null);
const [isActivateBotDialogOpen, setIsActivateBotDialogOpen] = useState(false);
const [selectedIntegrationOption, setSelectedIntegrationOption] =
useState<IntegrationOption | null>(null);
const router = useRouter();
const workspaceId = router.query.id as string;
type Props = {
frameworkIntegrations: typeof frameworkIntegrationOptions;
};
const Integration = ({ frameworkIntegrations }: Props) => {
const { t } = useTranslation();
useEffect(() => {
(async () => {
try {
const workspace = await getAWorkspace(workspaceId);
setEnvironments(workspace.environments);
// get cloud integration options
setCloudIntegrationOptions(await getIntegrationOptions());
// get project integration authorizations
setIntegrationAuths(
await getWorkspaceAuthorizations({
workspaceId
})
);
// get project integrations
setIntegrations(
await getWorkspaceIntegrations({
workspaceId
})
);
// get project bot
setBot(await getBot({ workspaceId }));
} catch (err) {
console.error(err);
}
})();
}, []);
/**
* Activate bot for project by performing the following steps:
* 1. Get the (encrypted) project key
* 2. Decrypt project key with user's private key
* 3. Encrypt project key with bot's public key
* 4. Send encrypted project key to backend and set bot status to active
*/
const handleBotActivate = async () => {
let botKey;
try {
if (bot) {
// case: there is a bot
const key = await getLatestFileKey({ workspaceId });
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY");
if (!PRIVATE_KEY) {
throw new Error("Private Key missing");
}
const WORKSPACE_KEY = decryptAssymmetric({
ciphertext: key.latestKey.encryptedKey,
nonce: key.latestKey.nonce,
publicKey: key.latestKey.sender.publicKey,
privateKey: PRIVATE_KEY
});
const { ciphertext, nonce } = encryptAssymmetric({
plaintext: WORKSPACE_KEY,
publicKey: bot.publicKey,
privateKey: PRIVATE_KEY
});
botKey = {
encryptedKey: ciphertext,
nonce
};
setBot(
(
await setBotActiveStatus({
botId: bot._id,
isActive: true,
botKey
})
).bot
);
}
} catch (err) {
console.error(err);
}
};
const handleUnauthorizedIntegrationOptionPress = (integrationOption: IntegrationOption) => {
try {
// generate CSRF token for OAuth2 code-token exchange integrations
const state = crypto.randomBytes(16).toString("hex");
localStorage.setItem("latestCSRFToken", state);
let link = "";
switch (integrationOption.slug) {
case "azure-key-vault":
link = `https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=${integrationOption.clientId}&response_type=code&redirect_uri=${window.location.origin}/integrations/azure-key-vault/oauth2/callback&response_mode=query&scope=https://vault.azure.net/.default openid offline_access&state=${state}`;
break;
case "aws-parameter-store":
link = `${window.location.origin}/integrations/aws-parameter-store/authorize`;
break;
case "aws-secret-manager":
link = `${window.location.origin}/integrations/aws-secret-manager/authorize`;
break;
case "heroku":
link = `https://id.heroku.com/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&scope=write-protected&state=${state}`;
break;
case "vercel":
link = `https://vercel.com/integrations/${integrationOption.clientSlug}/new?state=${state}`;
break;
case "netlify":
link = `https://app.netlify.com/authorize?client_id=${integrationOption.clientId}&response_type=code&state=${state}&redirect_uri=${window.location.origin}/integrations/netlify/oauth2/callback`;
break;
case "github":
link = `https://github.com/login/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&scope=repo&redirect_uri=${window.location.origin}/integrations/github/oauth2/callback&state=${state}`;
break;
case "gitlab":
link = `https://gitlab.com/oauth/authorize?client_id=${integrationOption.clientId}&redirect_uri=${window.location.origin}/integrations/gitlab/oauth2/callback&response_type=code&state=${state}`;
break;
case "render":
link = `${window.location.origin}/integrations/render/authorize`;
break;
case "flyio":
link = `${window.location.origin}/integrations/flyio/authorize`;
break;
case "circleci":
link = `${window.location.origin}/integrations/circleci/authorize`;
break;
case "travisci":
link = `${window.location.origin}/integrations/travisci/authorize`;
break;
case "supabase":
link = `${window.location.origin}/integrations/supabase/authorize`;
break;
case "checkly":
link = `${window.location.origin}/integrations/checkly/authorize`;
break;
case "railway":
link = `${window.location.origin}/integrations/railway/authorize`;
break;
case "hashicorp-vault":
link = `${window.location.origin}/integrations/hashicorp-vault/authorize`;
break;
case "cloudflare-pages":
link = `${window.location.origin}/integrations/cloudflare-pages/authorize`;
break;
default:
break;
}
if (link !== "") {
window.location.assign(link);
}
} catch (err) {
console.error(err);
}
};
const handleAuthorizedIntegrationOptionPress = (integrationAuth: IntegrationAuth) => {
try {
let link = "";
switch (integrationAuth.integration) {
case "azure-key-vault":
link = `${window.location.origin}/integrations/azure-key-vault/create?integrationAuthId=${integrationAuth._id}`;
break;
case "aws-parameter-store":
link = `${window.location.origin}/integrations/aws-parameter-store/create?integrationAuthId=${integrationAuth._id}`;
break;
case "aws-secret-manager":
link = `${window.location.origin}/integrations/aws-secret-manager/create?integrationAuthId=${integrationAuth._id}`;
break;
case "heroku":
link = `${window.location.origin}/integrations/heroku/create?integrationAuthId=${integrationAuth._id}`;
break;
case "vercel":
link = `${window.location.origin}/integrations/vercel/create?integrationAuthId=${integrationAuth._id}`;
break;
case "netlify":
link = `${window.location.origin}/integrations/netlify/create?integrationAuthId=${integrationAuth._id}`;
break;
case "github":
link = `${window.location.origin}/integrations/github/create?integrationAuthId=${integrationAuth._id}`;
break;
case "gitlab":
link = `${window.location.origin}/integrations/gitlab/create?integrationAuthId=${integrationAuth._id}`;
break;
case "render":
link = `${window.location.origin}/integrations/render/create?integrationAuthId=${integrationAuth._id}`;
break;
case "flyio":
link = `${window.location.origin}/integrations/flyio/create?integrationAuthId=${integrationAuth._id}`;
break;
case "circleci":
link = `${window.location.origin}/integrations/circleci/create?integrationAuthId=${integrationAuth._id}`;
break;
case "travisci":
link = `${window.location.origin}/integrations/travisci/create?integrationAuthId=${integrationAuth._id}`;
break;
case "supabase":
link = `${window.location.origin}/integrations/supabase/create?integrationAuthId=${integrationAuth._id}`;
break;
case "checkly":
link = `${window.location.origin}/integrations/checkly/create?integrationAuthId=${integrationAuth._id}`;
break;
case "railway":
link = `${window.location.origin}/integrations/railway/create?integrationAuthId=${integrationAuth._id}`;
break;
case "hashicorp-vault":
link = `${window.location.origin}/integrations/hashicorp-vault/create?integrationAuthId=${integrationAuth._id}`;
break;
case "cloudflare-pages":
link = `${window.location.origin}/integrations/cloudflare-pages/create?integrationAuthId=${integrationAuth._id}`;
break;
default:
break;
}
if (link !== "") {
window.location.assign(link);
}
} catch (err) {
console.error(err);
}
};
/**
* Open dialog to activate bot if bot is not active.
* Otherwise, start integration [integrationOption]
* @param {Object} integrationOption - an integration option
* @param {String} integrationOption.name
* @param {String} integrationOption.type
* @param {String} integrationOption.docsLink
* @returns
*/
const integrationOptionPress = async (integrationOption: IntegrationOption) => {
try {
const integrationAuthX = integrationAuths.find(
(integrationAuth) => integrationAuth.integration === integrationOption.slug
);
if (!bot.isActive) {
await handleBotActivate();
}
if (!integrationAuthX) {
// case: integration has not been authorized
handleUnauthorizedIntegrationOptionPress(integrationOption);
return;
}
handleAuthorizedIntegrationOptionPress(integrationAuthX);
} catch (err) {
console.error(err);
}
};
/**
* Handle deleting integration authorization [integrationAuth] and corresponding integrations from state where applicable
* @param {Object} obj
* @param {IntegrationAuth} obj.integrationAuth - integrationAuth to delete
*/
const handleDeleteIntegrationAuth = async ({
integrationAuth: deletedIntegrationAuth
}: {
integrationAuth: IntegrationAuth;
}) => {
try {
const newIntegrations = integrations.filter(
(integration) => integration.integrationAuth !== deletedIntegrationAuth._id
);
setIntegrationAuths(
integrationAuths.filter(
(integrationAuth) => integrationAuth._id !== deletedIntegrationAuth._id
)
);
setIntegrations(newIntegrations);
// handle updating bot
if (newIntegrations.length < 1) {
// case: no integrations left
setBot(
(
await setBotActiveStatus({
botId: bot._id,
isActive: false
})
).bot
);
}
} catch (err) {
console.error(err);
}
};
/**
* Handle deleting integration [integration]
* @param {Object} obj
* @param {Integration} obj.integration - integration to delete
*/
const handleDeleteIntegration = async ({ integration }: { integration: Integration }) => {
try {
const deletedIntegration = await deleteIntegration({
integrationId: integration._id
});
const newIntegrations = integrations.filter((i) => i._id !== deletedIntegration._id);
setIntegrations(newIntegrations);
// handle updating bot
if (newIntegrations.length < 1) {
// case: no integrations left
setBot(
(
await setBotActiveStatus({
botId: bot._id,
isActive: false
})
).bot
);
}
} catch (err) {
console.error(err);
}
};
return (
<div className="flex max-h-full flex-col justify-between bg-bunker-800 text-white">
<>
<Head>
<title>{t("common.head-title", { title: t("integrations.title") })}</title>
<link rel="icon" href="/infisical.ico" />
@@ -411,54 +20,26 @@ export default function Integrations() {
<meta property="og:title" content="Manage your .env files in seconds" />
<meta name="og:description" content={t("integrations.description") as string} />
</Head>
<div className="no-scrollbar::-webkit-scrollbar h-screen max-h-[calc(100vh-10px)] w-full overflow-y-scroll pb-6 no-scrollbar">
<div className="ml-6">
<NavHeader pageName={t("integrations.title")} isProjectRelated />
</div>
<ActivateBotDialog
isOpen={isActivateBotDialogOpen}
closeModal={() => setIsActivateBotDialogOpen(false)}
selectedIntegrationOption={selectedIntegrationOption}
integrationOptionPress={integrationOptionPress}
/>
<IntegrationSection
integrations={integrations}
setIntegrations={setIntegrations}
bot={bot}
setBot={setBot}
environments={environments}
handleDeleteIntegration={handleDeleteIntegration}
/>
{cloudIntegrationOptions.length > 0 && bot ? (
<CloudIntegrationSection
cloudIntegrationOptions={cloudIntegrationOptions}
setSelectedIntegrationOption={setSelectedIntegrationOption as any}
integrationOptionPress={(integrationOption: IntegrationOption) => {
if (!bot.isActive) {
// case: bot is not active -> open modal to activate bot
setIsActivateBotDialogOpen(true);
return;
}
integrationOptionPress(integrationOption);
}}
integrationAuths={integrationAuths}
handleDeleteIntegrationAuth={handleDeleteIntegrationAuth}
/>
) : (
<>
<div className="m-4 mt-7 flex max-w-5xl flex-col items-start justify-between px-2 text-xl">
<h1 className="text-3xl font-semibold">{t("integrations.cloud-integrations")}</h1>
<p className="text-base text-gray-400">{t("integrations.click-to-start")}</p>
</div>
<div className="mx-6 grid max-w-5xl grid-cols-4 grid-rows-2 gap-4">
{[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16].map(elem => <div key={elem} className="bg-mineshaft-800 border border-mineshaft-600 animate-pulse h-32 rounded-md"/>)}
</div>
</>
)}
<FrameworkIntegrationSection frameworks={frameworkIntegrationOptions as any} />
</div>
</div>
<IntegrationsPage frameworkIntegrations={frameworkIntegrations} />
</>
);
}
};
Integrations.requireAuth = true;
export const getStaticProps = () => {
return {
props: {
frameworkIntegrations: frameworkIntegrationOptions
}
};
};
export const getStaticPaths = async () => {
return {
paths: [], // indicates that no page needs be created at build time
fallback: "blocking" // indicates the type of fallback
};
};
Integration.requireAuth = true;
export default Integration;

View File

@@ -98,7 +98,7 @@ export default function ChecklyCreateIntegrationPage() {
>
Checkly Integration
</CardTitle>
<FormControl label="Infisical Project Environment" className="mt-2 px-6">
<FormControl label="Infisical Project Environment" className="mt-4 px-6">
<Select
value={selectedSourceEnvironment}
onValueChange={(val) => setSelectedSourceEnvironment(val)}
@@ -114,7 +114,7 @@ export default function ChecklyCreateIntegrationPage() {
))}
</Select>
</FormControl>
<FormControl label="Secrets Path">
<FormControl label="Secrets Path" className="mt-4 px-6">
<Input
value={secretPath}
onChange={(evt) => setSecretPath(evt.target.value)}

View File

@@ -0,0 +1,105 @@
import crypto from "crypto";
import { TCloudIntegration,UserWsKeyPair } from "@app/hooks/api/types";
import {
decryptAssymmetric,
encryptAssymmetric
} from "../../components/utilities/cryptography/crypto";
export const generateBotKey = (botPublicKey: string, latestKey: UserWsKeyPair) => {
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY");
if (!PRIVATE_KEY) {
throw new Error("Private Key missing");
}
const WORKSPACE_KEY = decryptAssymmetric({
ciphertext: latestKey.encryptedKey,
nonce: latestKey.nonce,
publicKey: latestKey.sender.publicKey,
privateKey: PRIVATE_KEY
});
const { ciphertext, nonce } = encryptAssymmetric({
plaintext: WORKSPACE_KEY,
publicKey: botPublicKey,
privateKey: PRIVATE_KEY
});
return { encryptedKey: ciphertext, nonce };
};
export const redirectForProviderAuth = (integrationOption: TCloudIntegration) => {
try {
// generate CSRF token for OAuth2 code-token exchange integrations
const state = crypto.randomBytes(16).toString("hex");
localStorage.setItem("latestCSRFToken", state);
let link = "";
switch (integrationOption.slug) {
case "azure-key-vault":
link = `https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=${integrationOption.clientId}&response_type=code&redirect_uri=${window.location.origin}/integrations/azure-key-vault/oauth2/callback&response_mode=query&scope=https://vault.azure.net/.default openid offline_access&state=${state}`;
break;
case "aws-parameter-store":
link = `${window.location.origin}/integrations/aws-parameter-store/authorize`;
break;
case "aws-secret-manager":
link = `${window.location.origin}/integrations/aws-secret-manager/authorize`;
break;
case "heroku":
link = `https://id.heroku.com/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&scope=write-protected&state=${state}`;
break;
case "vercel":
link = `https://vercel.com/integrations/${integrationOption.clientSlug}/new?state=${state}`;
break;
case "netlify":
link = `https://app.netlify.com/authorize?client_id=${integrationOption.clientId}&response_type=code&state=${state}&redirect_uri=${window.location.origin}/integrations/netlify/oauth2/callback`;
break;
case "github":
link = `https://github.com/login/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&scope=repo&redirect_uri=${window.location.origin}/integrations/github/oauth2/callback&state=${state}`;
break;
case "gitlab":
link = `https://gitlab.com/oauth/authorize?client_id=${integrationOption.clientId}&redirect_uri=${window.location.origin}/integrations/gitlab/oauth2/callback&response_type=code&state=${state}`;
break;
case "render":
link = `${window.location.origin}/integrations/render/authorize`;
break;
case "flyio":
link = `${window.location.origin}/integrations/flyio/authorize`;
break;
case "circleci":
link = `${window.location.origin}/integrations/circleci/authorize`;
break;
case "travisci":
link = `${window.location.origin}/integrations/travisci/authorize`;
break;
case "supabase":
link = `${window.location.origin}/integrations/supabase/authorize`;
break;
case "checkly":
link = `${window.location.origin}/integrations/checkly/authorize`;
break;
case "railway":
link = `${window.location.origin}/integrations/railway/authorize`;
break;
case "hashicorp-vault":
link = `${window.location.origin}/integrations/hashicorp-vault/authorize`;
break;
case "cloudflare-pages":
link = `${window.location.origin}/integrations/cloudflare-pages/authorize`;
break;
default:
break;
}
if (link !== "") {
window.location.assign(link);
}
} catch (err) {
console.error(err);
}
};
export const redirectToIntegrationAppConfigScreen = (provider: string, integrationAuthId: string) =>
`/integrations/${provider}/create?integrationAuthId=${integrationAuthId}`;

View File

@@ -0,0 +1,224 @@
import { useCallback, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { useRouter } from "next/router";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import NavHeader from "@app/components/navigation/NavHeader";
import { Button, Modal, ModalContent } from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { usePopUp } from "@app/hooks";
import {
useDeleteIntegration,
useDeleteIntegrationAuth,
useGetCloudIntegrations,
useGetUserWsKey,
useGetWorkspaceAuthorizations,
useGetWorkspaceBot,
useGetWorkspaceIntegrations,
useUpdateBotActiveStatus
} from "@app/hooks/api";
import { IntegrationAuth } from "@app/hooks/api/types";
import { CloudIntegrationSection } from "./components/CloudIntegrationSection";
import { FrameworkIntegrationSection } from "./components/FrameworkIntegrationSection";
import { IntegrationsSection } from "./components/IntegrationsSection";
import {
generateBotKey,
redirectForProviderAuth,
redirectToIntegrationAppConfigScreen
} from "./IntegrationPage.utils";
type Props = {
frameworkIntegrations: Array<{ name: string; slug: string; image: string; docsLink: string }>;
};
export const IntegrationsPage = ({ frameworkIntegrations }: Props) => {
const { t } = useTranslation();
const { createNotification } = useNotificationContext();
const router = useRouter();
const { currentWorkspace } = useWorkspace();
const workspaceId = currentWorkspace?._id || "";
const environments = currentWorkspace?.environments || [];
const { data: latestWsKey } = useGetUserWsKey(workspaceId);
const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([
"activeBot"
] as const);
const { data: cloudIntegrations, isLoading: isCloudIntegrationsLoading } =
useGetCloudIntegrations();
const { data: integrationAuths, isLoading: isIntegrationAuthLoading } =
useGetWorkspaceAuthorizations(
workspaceId,
useCallback((data: IntegrationAuth[]) => {
const groupBy: Record<string, IntegrationAuth> = {};
data.forEach((el) => {
groupBy[el.integration] = el;
});
return groupBy;
}, [])
);
// mutation
const {
data: integrations,
isLoading: isIntegrationLoading,
isFetching: isIntegrationFetching
} = useGetWorkspaceIntegrations(workspaceId);
const { data: bot } = useGetWorkspaceBot(workspaceId);
// mutation
const { mutateAsync: updateBotActiveStatus, mutate: updateBotActiveStatusSync } =
useUpdateBotActiveStatus();
const { mutateAsync: deleteIntegration } = useDeleteIntegration();
const {
mutateAsync: deleteIntegrationAuth,
isLoading: isDeleteIntegrationAuthSuccess,
reset: resetDeleteIntegrationAuth
} = useDeleteIntegrationAuth();
// summary: this use effect is trigger when all integration auths are removed thus deactivate bot
// details: so onsuccessfully deleting an integration auth, immediately integration list is refeteched
// After the refetch is completed check if its empty. Then set bot active and reset the submit hook
useEffect(() => {
if (isDeleteIntegrationAuthSuccess && !isIntegrationFetching && !integrations?.length) {
if (bot?._id)
updateBotActiveStatusSync({
isActive: false,
botId: bot._id,
workspaceId
});
resetDeleteIntegrationAuth();
}
}, [isIntegrationFetching, isDeleteIntegrationAuthSuccess, integrations?.length]);
const handleProviderIntegration = async (provider: string) => {
const selectedCloudIntegration = cloudIntegrations?.find(({ slug }) => provider === slug);
if (!selectedCloudIntegration) return;
try {
if (bot && !bot.isActive) {
const botKey = generateBotKey(bot.publicKey, latestWsKey!);
await updateBotActiveStatus({
workspaceId,
botKey,
isActive: true,
botId: bot._id
});
}
const integrationAuthForProvider = integrationAuths?.[provider];
if (!integrationAuthForProvider) {
redirectForProviderAuth(selectedCloudIntegration);
return;
}
const url = redirectToIntegrationAppConfigScreen(provider, integrationAuthForProvider._id);
router.push(url);
} catch (error) {
console.error(error);
}
};
// function to strat integration for a provider
// confirmation to user passing the bot key for provider to get secret access
const handleProviderIntegrationStart = (provider: string) => {
if (!bot?.isActive) {
handlePopUpOpen("activeBot", { provider });
return;
}
handleProviderIntegration(provider);
};
const handleUserAcceptBotCondition = () => {
const { provider } = popUp.activeBot?.data as { provider: string };
handleProviderIntegration(provider);
handlePopUpClose("activeBot");
};
const handleIntegrationDelete = async (integrationId: string, cb: () => void) => {
try {
await deleteIntegration({ id: integrationId, workspaceId });
if (cb) cb();
createNotification({
type: "success",
text: "Deleted integration"
});
} catch (err) {
console.log(err);
createNotification({
type: "error",
text: "Failed to delete integration"
});
}
};
const handleIntegrationAuthRevoke = async (provider: string, cb?: () => void) => {
const integrationAuthForProvider = integrationAuths?.[provider];
if (!integrationAuthForProvider) return;
try {
await deleteIntegrationAuth({
id: integrationAuthForProvider._id,
workspaceId
});
if (cb) cb();
createNotification({
type: "success",
text: "Revoked provider authentication"
});
} catch (err) {
console.error(err);
createNotification({
type: "error",
text: "Failed to revoke provider authentication"
});
}
};
return (
<div className="container mx-auto max-w-7xl px-8 pb-12 text-white">
<div className="ml-4">
<NavHeader pageName={t("integrations.title")} isProjectRelated />
</div>
<IntegrationsSection
isLoading={isIntegrationLoading}
integrations={integrations}
environments={environments}
onIntegrationDelete={({ _id: id }, cb) => handleIntegrationDelete(id, cb)}
/>
<CloudIntegrationSection
isLoading={isCloudIntegrationsLoading || isIntegrationAuthLoading}
cloudIntegrations={cloudIntegrations}
integrationAuths={integrationAuths}
onIntegrationStart={handleProviderIntegrationStart}
onIntegrationRevoke={handleIntegrationAuthRevoke}
/>
<Modal
isOpen={popUp.activeBot?.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("activeBot", isOpen)}
>
<ModalContent
title={t("integrations.grant-access-to-secrets") as string}
footerContent={
<div className="flex items-center space-x-2">
<Button onClick={() => handleUserAcceptBotCondition()}>
{t("integrations.grant-access-button") as string}
</Button>
<Button
onClick={() => handlePopUpClose("activeBot")}
variant="outline_bg"
colorSchema="secondary"
>
Cancel
</Button>
</div>
}
>
{t("integrations.why-infisical-needs-access")}
</ModalContent>
</Modal>
<FrameworkIntegrationSection frameworks={frameworkIntegrations} />
</div>
);
};

View File

@@ -0,0 +1,140 @@
import { useTranslation } from "react-i18next";
import { faCheck, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { DeleteActionModal,Skeleton, Tooltip } from "@app/components/v2";
import { usePopUp } from "@app/hooks";
import { IntegrationAuth, TCloudIntegration } from "@app/hooks/api/types";
type Props = {
isLoading?: boolean;
integrationAuths?: Record<string, IntegrationAuth>;
cloudIntegrations?: TCloudIntegration[];
onIntegrationStart: (slug: string) => void;
// cb: handle popUpClose child->parent communication pattern
onIntegrationRevoke: (slug: string, cb: () => void) => void;
};
type TRevokeIntegrationPopUp = { provider: string };
export const CloudIntegrationSection = ({
isLoading,
cloudIntegrations = [],
integrationAuths = {},
onIntegrationStart,
onIntegrationRevoke
}: Props) => {
const { t } = useTranslation();
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"deleteConfirmation"
] as const);
const isEmpty = !isLoading && !cloudIntegrations?.length;
return (
<div>
<div className="m-4 mt-7 flex max-w-5xl flex-col items-start justify-between px-2 text-xl">
<h1 className="text-3xl font-semibold">{t("integrations.cloud-integrations")}</h1>
<p className="text-base text-gray-400">{t("integrations.click-to-start")}</p>
</div>
<div
className="mx-6 grid grid-flow-dense gap-4"
style={{ gridTemplateColumns: "repeat(auto-fill, minmax(256px, 1fr))" }}
>
{isLoading &&
Array.from({ length: 12 }).map((_, index) => (
<Skeleton className="h-32" key={`cloud-integration-skeleton-${index + 1}`} />
))}
{!isLoading &&
cloudIntegrations?.map((cloudIntegration) => (
<div
onKeyDown={() => null}
role="button"
tabIndex={0}
className={`group relative ${
cloudIntegration.isAvailable
? "cursor-pointer duration-200 hover:bg-mineshaft-700"
: "opacity-50"
} flex h-32 flex-row items-center rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4`}
onClick={() => {
if (!cloudIntegration.isAvailable) return;
onIntegrationStart(cloudIntegration.slug);
}}
key={cloudIntegration.slug}
>
<img
src={`/images/integrations/${cloudIntegration.image}`}
height={70}
width={70}
alt="integration logo"
/>
{cloudIntegration.name.split(" ").length > 2 ? (
<div className="ml-4 max-w-xs text-3xl font-semibold text-gray-300 duration-200 group-hover:text-gray-200">
<div>{cloudIntegration.name.split(" ")[0]}</div>
<div className="text-base">
{cloudIntegration.name.split(" ")[1]} {cloudIntegration.name.split(" ")[2]}
</div>
</div>
) : (
<div className="ml-4 max-w-xs text-xl font-semibold text-gray-300 duration-200 group-hover:text-gray-200">
{cloudIntegration.name}
</div>
)}
{cloudIntegration.isAvailable &&
Boolean(integrationAuths?.[cloudIntegration.slug]) && (
<div className="absolute top-0 right-0 z-40 h-full">
<div className="relative h-full">
<div className="absolute top-0 right-0 w-24 flex-row items-center overflow-hidden whitespace-nowrap rounded-tr-md rounded-bl-md bg-primary py-0.5 px-2 text-xs text-black opacity-80 transition-all duration-300 group-hover:w-0 group-hover:p-0">
<FontAwesomeIcon icon={faCheck} className="mr-2 text-xs" />
Authorized
</div>
<Tooltip content="Revoke Access">
<div
onKeyDown={() => null}
role="button"
tabIndex={0}
onClick={async (event) => {
event.stopPropagation();
handlePopUpOpen("deleteConfirmation", {
provider: cloudIntegration.slug
});
}}
className="absolute top-0 right-0 flex h-0 w-12 cursor-pointer items-center justify-center overflow-hidden rounded-r-md bg-red text-xs opacity-50 transition-all duration-300 hover:opacity-100 group-hover:h-full"
>
<FontAwesomeIcon icon={faXmark} size="xl" />
</div>
</Tooltip>
</div>
</div>
)}
</div>
))}
</div>
{isEmpty && (
<div className="mx-6 grid max-w-5xl grid-cols-4 grid-rows-2 gap-4">
{Array.from({ length: 16 }).map((_, index) => (
<div
key={`dummy-cloud-integration-${index + 1}`}
className="h-32 animate-pulse rounded-md border border-mineshaft-600 bg-mineshaft-800"
/>
))}
</div>
)}
<DeleteActionModal
isOpen={popUp.deleteConfirmation.isOpen}
title={`Are you sure want to revoke access ${
(popUp?.deleteConfirmation.data as TRevokeIntegrationPopUp)?.provider || " "
}?`}
subTitle="This will remove all the secret integration of this provider!!!"
onChange={(isOpen) => handlePopUpToggle("deleteConfirmation", isOpen)}
deleteKey={(popUp?.deleteConfirmation?.data as TRevokeIntegrationPopUp)?.provider || ""}
onDeleteApproved={async () => {
onIntegrationRevoke(
(popUp.deleteConfirmation.data as TRevokeIntegrationPopUp)?.provider,
() => handlePopUpClose("deleteConfirmation")
);
}}
/>
</div>
);
};

View File

@@ -0,0 +1 @@
export { CloudIntegrationSection } from "./CloudIntegrationSection";

View File

@@ -0,0 +1,54 @@
import { useTranslation } from "react-i18next";
type Props = {
frameworks: Array<{
name: string;
image: string;
slug: string;
docsLink: string;
}>;
};
export const FrameworkIntegrationSection = ({ frameworks }: Props) => {
const { t } = useTranslation();
return (
<>
<div className="mx-4 mt-12 mb-4 flex flex-col items-start justify-between px-2 text-xl">
<h1 className="text-3xl font-semibold">{t("integrations.framework-integrations")}</h1>
<p className="text-base text-gray-400">{t("integrations.click-to-setup")}</p>
</div>
<div
className="mx-6 mt-4 grid grid-flow-dense gap-3"
style={{ gridTemplateColumns: "repeat(auto-fill, minmax(120px, 1fr))" }}
>
{frameworks.map((framework) => (
<a
key={`framework-integration-${framework.slug}`}
href={framework.docsLink}
rel="noopener noreferrer"
target="_blank"
className="relative flex h-32 cursor-pointer flex-row items-center justify-center rounded-md p-0.5 duration-200"
>
<div
className={`flex h-full w-full cursor-pointer flex-col items-center justify-center rounded-md border border-mineshaft-600 bg-mineshaft-800 font-semibold text-gray-300 duration-200 hover:bg-mineshaft-700 group-hover:text-gray-200 ${
framework?.name?.split(" ").length > 1 ? "px-1 text-sm" : "px-2 text-xl"
} w-full max-w-xs text-center`}
>
{framework?.image && (
<img
src={`/images/integrations/${framework.image}.png`}
height={framework?.name ? 60 : 90}
width={framework?.name ? 60 : 90}
alt="integration logo"
/>
)}
{framework?.name && framework?.image && <div className="h-2" />}
{framework?.name && framework.name}
</div>
</a>
))}
</div>
</>
);
};

View File

@@ -0,0 +1 @@
export { FrameworkIntegrationSection } from "./FrameworkIntegrationSection";

View File

@@ -0,0 +1,149 @@
import { faArrowRight, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { integrationSlugNameMapping } from "public/data/frequentConstants";
import {
DeleteActionModal,
EmptyState,
FormControl,
FormLabel,
IconButton,
Select,
SelectItem,
Skeleton,
Tooltip
} from "@app/components/v2";
import { usePopUp } from "@app/hooks";
import { TIntegration } from "@app/hooks/api/types";
type Props = {
environments: Array<{ name: string; slug: string }>;
integrations?: TIntegration[];
isLoading?: boolean;
onIntegrationDelete: (integration: TIntegration, cb: () => void) => void;
};
export const IntegrationsSection = ({
integrations = [],
environments = [],
isLoading,
onIntegrationDelete
}: Props) => {
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"deleteConfirmation"
] as const);
return (
<div className="mb-8">
<div className="mx-4 mb-4 mt-6 flex flex-col items-start justify-between px-2 text-xl">
<h1 className="text-3xl font-semibold">Current Integrations</h1>
<p className="text-base text-bunker-300">Manage integrations with third-party services.</p>
</div>
{isLoading && (
<div className="p-6 pt-0">
<Skeleton className="h-28" />
</div>
)}
{!isLoading && !integrations.length && (
<EmptyState
className="mx-6 rounded-md border border-mineshaft-700 pt-8 pb-4"
title="No integrations found. Click on one of the below providers to sync secrets."
/>
)}
{!isLoading && (
<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"
key={`integration-${integration?._id.toString()}`}
>
<div className="flex">
<div>
<FormControl label="Environment">
<Select
value={integration.environment}
isDisabled={integration.isActive}
className="min-w-[8rem] border border-mineshaft-700"
>
{environments.map((environment) => {
return (
<SelectItem
value={environment.slug}
key={`environment-${environment.slug}`}
>
{environment.name}
</SelectItem>
);
})}
</Select>
</FormControl>
</div>
<div className="ml-2 flex flex-col">
<FormLabel label="Secret Path" />
<div className="min-w-[8rem] rounded-md border border-mineshaft-700 bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200">
{integration.secretPath}
</div>
</div>
<div className="flex h-full items-center">
<FontAwesomeIcon icon={faArrowRight} className="mx-4 text-gray-400" />
</div>
<div className="ml-4 flex flex-col">
<FormLabel label="Integration" />
<div className="min-w-[8rem] rounded-md border border-mineshaft-700 bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200">
{integrationSlugNameMapping[integration.integration]}
</div>
</div>
<div className="ml-2 flex flex-col">
<FormLabel label="App" />
<div className="min-w-[8rem] rounded-md border border-mineshaft-700 bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200">
{integration.integration === "hashicorp-vault"
? `${integration.app} - path: ${integration.path}`
: integration.app}
</div>
</div>
{(integration.integration === "vercel" ||
integration.integration === "netlify" ||
integration.integration === "railway" ||
integration.integration === "gitlab") && (
<div className="ml-4 flex flex-col">
<FormLabel label="Target Environment" />
<div className="rounded-md border border-mineshaft-700 bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200">
{integration.targetEnvironment}
</div>
</div>
)}
</div>
<div className="flex cursor-default items-center">
<div className="ml-2 opacity-80 duration-200 hover:opacity-100">
<Tooltip content="Remove Integration">
<IconButton
onClick={() => handlePopUpOpen("deleteConfirmation", integration)}
ariaLabel="delete"
colorSchema="danger"
variant="star"
>
<FontAwesomeIcon icon={faXmark} className="px-0.5" />
</IconButton>
</Tooltip>
</div>
</div>
</div>
))}
</div>
)}
<DeleteActionModal
isOpen={popUp.deleteConfirmation.isOpen}
title={`Are you sure want to remove ${
(popUp?.deleteConfirmation.data as TIntegration)?.integration || " "
} integration for ${(popUp?.deleteConfirmation.data as TIntegration)?.app || " "}?`}
onChange={(isOpen) => handlePopUpToggle("deleteConfirmation", isOpen)}
deleteKey={(popUp?.deleteConfirmation?.data as TIntegration)?.app || ""}
onDeleteApproved={async () =>
onIntegrationDelete(popUp?.deleteConfirmation.data as TIntegration, () =>
handlePopUpClose("deleteConfirmation")
)
}
/>
</div>
);
};

View File

@@ -0,0 +1 @@
export { IntegrationsSection } from "./IntegrationsSection";

View File

@@ -0,0 +1 @@
export { IntegrationsPage } from "./IntegrationsPage";

View File

@@ -18,4 +18,4 @@ version: 0.1.6
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
appVersion: "0.1.6"
appVersion: "0.1.7"

View File

@@ -127,7 +127,7 @@ func (r *InfisicalSecretReconciler) GetInfisicalServiceAccountCredentialsFromKub
return model.ServiceAccountDetails{AccessKey: string(accessKeyFromSecret), PrivateKey: string(privateKeyFromSecret), PublicKey: string(publicKeyFromSecret)}, nil
}
func (r *InfisicalSecretReconciler) CreateInfisicalManagedKubeSecret(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret, secretsFromAPI []model.SingleEnvironmentVariable, encryptedSecretsResponse api.GetEncryptedSecretsV2Response) error {
func (r *InfisicalSecretReconciler) CreateInfisicalManagedKubeSecret(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret, secretsFromAPI []model.SingleEnvironmentVariable, encryptedSecretsResponse api.GetEncryptedSecretsV3Response) error {
plainProcessedSecrets := make(map[string][]byte)
for _, secret := range secretsFromAPI {
plainProcessedSecrets[secret.Key] = []byte(secret.Value) // plain process
@@ -155,7 +155,7 @@ func (r *InfisicalSecretReconciler) CreateInfisicalManagedKubeSecret(ctx context
return nil
}
func (r *InfisicalSecretReconciler) UpdateInfisicalManagedKubeSecret(ctx context.Context, managedKubeSecret corev1.Secret, secretsFromAPI []model.SingleEnvironmentVariable, encryptedSecretsResponse api.GetEncryptedSecretsV2Response) error {
func (r *InfisicalSecretReconciler) UpdateInfisicalManagedKubeSecret(ctx context.Context, managedKubeSecret corev1.Secret, secretsFromAPI []model.SingleEnvironmentVariable, encryptedSecretsResponse api.GetEncryptedSecretsV3Response) error {
plainProcessedSecrets := make(map[string][]byte)
for _, secret := range secretsFromAPI {
plainProcessedSecrets[secret.Key] = []byte(secret.Value)
@@ -208,7 +208,7 @@ func (r *InfisicalSecretReconciler) ReconcileInfisicalSecret(ctx context.Context
}
var plainTextSecretsFromApi []model.SingleEnvironmentVariable
var fullEncryptedSecretsResponse api.GetEncryptedSecretsV2Response
var fullEncryptedSecretsResponse api.GetEncryptedSecretsV3Response
if serviceAccountCreds.AccessKey != "" || serviceAccountCreds.PrivateKey != "" || serviceAccountCreds.PublicKey != "" {
plainTextSecretsFromApi, fullEncryptedSecretsResponse, err = util.GetPlainTextSecretsViaServiceAccount(serviceAccountCreds, infisicalSecret.Spec.Authentication.ServiceAccount.ProjectId, infisicalSecret.Spec.Authentication.ServiceAccount.EnvironmentName, secretVersionBasedOnETag)

View File

@@ -47,36 +47,40 @@ func CallGetServiceTokenDetailsV2(httpClient *resty.Client) (GetServiceTokenDeta
return tokenDetailsResponse, nil
}
func CallGetSecretsV2(httpClient *resty.Client, request GetEncryptedSecretsV2Request) (GetEncryptedSecretsV2Response, error) {
var encryptedSecretsResponse GetEncryptedSecretsV2Response
createHttpRequest := httpClient.
func CallGetSecretsV3(httpClient *resty.Client, request GetEncryptedSecretsV3Request) (GetEncryptedSecretsV3Response, error) {
var secretsResponse GetEncryptedSecretsV3Response
httpRequest := httpClient.
R().
SetResult(&secretsResponse).
SetHeader("User-Agent", USER_AGENT_NAME).
SetHeader("If-None-Match", request.ETag).
SetQueryParam("environment", request.Environment).
SetQueryParam("workspaceId", request.WorkspaceId).
SetResult(&encryptedSecretsResponse).
SetHeader("User-Agent", USER_AGENT_NAME)
SetQueryParam("workspaceId", request.WorkspaceId)
createHttpRequest.SetHeader("If-None-Match", request.ETag)
if request.SecretPath != "" {
httpRequest.SetQueryParam("secretPath", request.SecretPath)
}
response, err := createHttpRequest.Get(fmt.Sprintf("%v/v2/secrets", API_HOST_URL))
response, err := httpRequest.Get(fmt.Sprintf("%v/v3/secrets", API_HOST_URL))
if err != nil {
return GetEncryptedSecretsV2Response{}, fmt.Errorf("CallGetSecretsV2: Unable to complete api request [err=%s]", err)
return GetEncryptedSecretsV3Response{}, fmt.Errorf("CallGetSecretsV3: Unable to complete api request [err=%s]", err)
}
if response.IsError() {
return GetEncryptedSecretsV2Response{}, fmt.Errorf("CallGetSecretsV2: Unsuccessful response: [response=%s]", response)
return GetEncryptedSecretsV3Response{}, fmt.Errorf("CallGetSecretsV3: Unsuccessful response. Please make sure your secret path, workspace and environment name are all correct [response=%s]", response)
}
if response.StatusCode() == 304 {
encryptedSecretsResponse.Modified = false
secretsResponse.Modified = false
} else {
encryptedSecretsResponse.Modified = true
secretsResponse.Modified = true
}
encryptedSecretsResponse.ETag = response.Header().Get("etag")
secretsResponse.ETag = response.Header().Get("etag")
return encryptedSecretsResponse, nil
return secretsResponse, nil
}
func CallGetServiceTokenAccountDetailsV2(httpClient *resty.Client) (ServiceAccountDetailsResponse, error) {

View File

@@ -28,30 +28,42 @@ type GetEncryptedWorkspaceKeyResponse struct {
UpdatedAt time.Time `json:"updatedAt"`
}
type GetEncryptedSecretsV2Request struct {
type GetEncryptedSecretsV3Request struct {
Environment string `json:"environment"`
WorkspaceId string `json:"workspaceId"`
SecretPath string `json:"secretPath"`
ETag string `json:"etag,omitempty"`
}
type GetEncryptedSecretsV2Response struct {
type GetEncryptedSecretsV3Response struct {
Secrets []struct {
ID string `json:"_id"`
Version int `json:"version"`
Workspace string `json:"workspace"`
Type string `json:"type"`
Environment string `json:"environment"`
SecretKeyCiphertext string `json:"secretKeyCiphertext"`
SecretKeyIV string `json:"secretKeyIV"`
SecretKeyTag string `json:"secretKeyTag"`
SecretValueCiphertext string `json:"secretValueCiphertext"`
SecretValueIV string `json:"secretValueIV"`
SecretValueTag string `json:"secretValueTag"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
User string `json:"user,omitempty"`
ID string `json:"_id"`
Version int `json:"version"`
Workspace string `json:"workspace"`
Type string `json:"type"`
Tags []struct {
ID string `json:"_id"`
Name string `json:"name"`
Slug string `json:"slug"`
Workspace string `json:"workspace"`
} `json:"tags"`
Environment string `json:"environment"`
SecretKeyCiphertext string `json:"secretKeyCiphertext"`
SecretKeyIV string `json:"secretKeyIV"`
SecretKeyTag string `json:"secretKeyTag"`
SecretValueCiphertext string `json:"secretValueCiphertext"`
SecretValueIV string `json:"secretValueIV"`
SecretValueTag string `json:"secretValueTag"`
SecretCommentCiphertext string `json:"secretCommentCiphertext"`
SecretCommentIV string `json:"secretCommentIV"`
SecretCommentTag string `json:"secretCommentTag"`
Algorithm string `json:"algorithm"`
KeyEncoding string `json:"keyEncoding"`
Folder string `json:"folder"`
V int `json:"__v"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
} `json:"secrets"`
Modified bool `json:"modified,omitempty"`
ETag string `json:"ETag,omitempty"`
}
@@ -64,6 +76,7 @@ type GetServiceTokenDetailsResponse struct {
EncryptedKey string `json:"encryptedKey"`
Iv string `json:"iv"`
Tag string `json:"tag"`
SecretPath string `json:"secretPath"`
}
type ServiceAccountDetailsResponse struct {

View File

@@ -48,10 +48,10 @@ func GetServiceTokenDetails(infisicalToken string) (api.GetServiceTokenDetailsRe
return serviceTokenDetails, nil
}
func GetPlainTextSecretsViaServiceToken(fullServiceToken string, etag string) ([]model.SingleEnvironmentVariable, api.GetEncryptedSecretsV2Response, error) {
func GetPlainTextSecretsViaServiceToken(fullServiceToken string, etag string) ([]model.SingleEnvironmentVariable, api.GetEncryptedSecretsV3Response, error) {
serviceTokenParts := strings.SplitN(fullServiceToken, ".", 4)
if len(serviceTokenParts) < 4 {
return nil, api.GetEncryptedSecretsV2Response{}, fmt.Errorf("invalid service token entered. Please double check your service token and try again")
return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("invalid service token entered. Please double check your service token and try again")
}
serviceToken := fmt.Sprintf("%v.%v.%v", serviceTokenParts[0], serviceTokenParts[1], serviceTokenParts[2])
@@ -63,32 +63,33 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string, etag string) ([
serviceTokenDetails, err := api.CallGetServiceTokenDetailsV2(httpClient)
if err != nil {
return nil, api.GetEncryptedSecretsV2Response{}, fmt.Errorf("unable to get service token details. [err=%v]", err)
return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("unable to get service token details. [err=%v]", err)
}
encryptedSecretsResponse, err := api.CallGetSecretsV2(httpClient, api.GetEncryptedSecretsV2Request{
encryptedSecretsResponse, err := api.CallGetSecretsV3(httpClient, api.GetEncryptedSecretsV3Request{
WorkspaceId: serviceTokenDetails.Workspace,
Environment: serviceTokenDetails.Environment,
ETag: etag,
SecretPath: serviceTokenDetails.SecretPath,
})
if err != nil {
return nil, api.GetEncryptedSecretsV2Response{}, err
return nil, api.GetEncryptedSecretsV3Response{}, err
}
decodedSymmetricEncryptionDetails, err := GetBase64DecodedSymmetricEncryptionDetails(serviceTokenParts[3], serviceTokenDetails.EncryptedKey, serviceTokenDetails.Iv, serviceTokenDetails.Tag)
if err != nil {
return nil, api.GetEncryptedSecretsV2Response{}, fmt.Errorf("unable to decode symmetric encryption details [err=%v]", err)
return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("unable to decode symmetric encryption details [err=%v]", err)
}
plainTextWorkspaceKey, err := crypto.DecryptSymmetric([]byte(serviceTokenParts[3]), decodedSymmetricEncryptionDetails.Cipher, decodedSymmetricEncryptionDetails.Tag, decodedSymmetricEncryptionDetails.IV)
if err != nil {
return nil, api.GetEncryptedSecretsV2Response{}, fmt.Errorf("unable to decrypt the required workspace key")
return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("unable to decrypt the required workspace key")
}
plainTextSecrets, err := GetPlainTextSecrets(plainTextWorkspaceKey, encryptedSecretsResponse)
if err != nil {
return nil, api.GetEncryptedSecretsV2Response{}, fmt.Errorf("unable to decrypt your secrets [err=%v]", err)
return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("unable to decrypt your secrets [err=%v]", err)
}
return plainTextSecrets, encryptedSecretsResponse, nil
@@ -97,19 +98,19 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string, etag string) ([
// Fetches plaintext secrets from an API endpoint using a service account.
// The function fetches the service account details and keys, decrypts the workspace key, fetches the encrypted secrets for the specified project and environment, and decrypts the secrets using the decrypted workspace key.
// Returns the plaintext secrets, encrypted secrets response, and any errors that occurred during the process.
func GetPlainTextSecretsViaServiceAccount(serviceAccountCreds model.ServiceAccountDetails, projectId string, environmentName string, etag string) ([]model.SingleEnvironmentVariable, api.GetEncryptedSecretsV2Response, error) {
func GetPlainTextSecretsViaServiceAccount(serviceAccountCreds model.ServiceAccountDetails, projectId string, environmentName string, etag string) ([]model.SingleEnvironmentVariable, api.GetEncryptedSecretsV3Response, error) {
httpClient := resty.New()
httpClient.SetAuthToken(serviceAccountCreds.AccessKey).
SetHeader("Accept", "application/json")
serviceAccountDetails, err := api.CallGetServiceTokenAccountDetailsV2(httpClient)
if err != nil {
return nil, api.GetEncryptedSecretsV2Response{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to get service account details. [err=%v]", err)
return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to get service account details. [err=%v]", err)
}
serviceAccountKeys, err := api.CallGetServiceAccountKeysV2(httpClient, api.GetServiceAccountKeysRequest{ServiceAccountId: serviceAccountDetails.ServiceAccount.ID})
if err != nil {
return nil, api.GetEncryptedSecretsV2Response{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to get service account key details. [err=%v]", err)
return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to get service account key details. [err=%v]", err)
}
// find key for requested project
@@ -121,45 +122,45 @@ func GetPlainTextSecretsViaServiceAccount(serviceAccountCreds model.ServiceAccou
}
if workspaceServiceAccountKey.ID == "" || workspaceServiceAccountKey.EncryptedKey == "" || workspaceServiceAccountKey.Nonce == "" || serviceAccountCreds.PublicKey == "" || serviceAccountCreds.PrivateKey == "" {
return nil, api.GetEncryptedSecretsV2Response{}, fmt.Errorf("unable to find key for [projectId=%s] [err=%v]. Ensure that the given service account has access to given projectId", projectId, err)
return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("unable to find key for [projectId=%s] [err=%v]. Ensure that the given service account has access to given projectId", projectId, err)
}
cipherText, err := base64.StdEncoding.DecodeString(workspaceServiceAccountKey.EncryptedKey)
if err != nil {
return nil, api.GetEncryptedSecretsV2Response{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to decode EncryptedKey secrets because [err=%v]", err)
return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to decode EncryptedKey secrets because [err=%v]", err)
}
nonce, err := base64.StdEncoding.DecodeString(workspaceServiceAccountKey.Nonce)
if err != nil {
return nil, api.GetEncryptedSecretsV2Response{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to decode nonce secrets because [err=%v]", err)
return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to decode nonce secrets because [err=%v]", err)
}
publickey, err := base64.StdEncoding.DecodeString(serviceAccountCreds.PublicKey)
if err != nil {
return nil, api.GetEncryptedSecretsV2Response{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to decode PublicKey secrets because [err=%v]", err)
return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to decode PublicKey secrets because [err=%v]", err)
}
privateKey, err := base64.StdEncoding.DecodeString(serviceAccountCreds.PrivateKey)
if err != nil {
return nil, api.GetEncryptedSecretsV2Response{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to decode PrivateKey secrets because [err=%v]", err)
return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to decode PrivateKey secrets because [err=%v]", err)
}
plainTextWorkspaceKey := crypto.DecryptAsymmetric(cipherText, nonce, publickey, privateKey)
encryptedSecretsResponse, err := api.CallGetSecretsV2(httpClient, api.GetEncryptedSecretsV2Request{
encryptedSecretsResponse, err := api.CallGetSecretsV3(httpClient, api.GetEncryptedSecretsV3Request{
WorkspaceId: projectId,
Environment: environmentName,
ETag: etag,
})
if err != nil {
return nil, api.GetEncryptedSecretsV2Response{}, fmt.Errorf("unable to fetch secrets because [err=%v]", err)
return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("unable to fetch secrets because [err=%v]", err)
}
plainTextSecrets, err := GetPlainTextSecrets(plainTextWorkspaceKey, encryptedSecretsResponse)
if err != nil {
return nil, api.GetEncryptedSecretsV2Response{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to get plain text secrets because [err=%v]", err)
return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to get plain text secrets because [err=%v]", err)
}
return plainTextSecrets, encryptedSecretsResponse, nil
@@ -194,7 +195,7 @@ func GetBase64DecodedSymmetricEncryptionDetails(key string, cipher string, IV st
}, nil
}
func GetPlainTextSecrets(key []byte, encryptedSecretsResponse api.GetEncryptedSecretsV2Response) ([]model.SingleEnvironmentVariable, error) {
func GetPlainTextSecrets(key []byte, encryptedSecretsResponse api.GetEncryptedSecretsV3Response) ([]model.SingleEnvironmentVariable, error) {
plainTextSecrets := []model.SingleEnvironmentVariable{}
for _, secret := range encryptedSecretsResponse.Secrets {
// Decrypt key