-
-
-
-
-
- Infisical
-
+
+ {subscription && subscription.status === "trialing" && subscription.trial_end && (
+
+
+ {`Currently trialing the ${formatPlanSlug(subscription.slug)} plan until ${formatDate(subscription.trial_end).formattedDate} - ${formatDate(subscription.trial_end).remainingDays} day(s) left. `}
+ Add a card to avoid being downgraded to the Starter plan afterward →
+
+
+ )}
);
-};
+};
\ No newline at end of file
diff --git a/frontend/src/pages/integrations/laravel-forge/authorize.tsx b/frontend/src/pages/integrations/laravel-forge/authorize.tsx
new file mode 100644
index 000000000..d69b263e9
--- /dev/null
+++ b/frontend/src/pages/integrations/laravel-forge/authorize.tsx
@@ -0,0 +1,80 @@
+import { useState } from "react";
+import { useRouter } from "next/router";
+
+import { Button, Card, CardTitle, FormControl, Input } from "../../../components/v2";
+import saveIntegrationAccessToken from "../../api/integrations/saveIntegrationAccessToken";
+
+export default function LaravelForgeCreateIntegrationPage() {
+ const router = useRouter();
+ const [apiKey, setApiKey] = useState("");
+ const [apiKeyErrorText, setApiKeyErrorText] = useState("");
+ const [serverId, setServerId] = useState("");
+ const [serverIdErrorText, setServerIdErrorText] = useState("");
+ const [isLoading, setIsLoading] = useState(false);
+
+ const handleButtonClick = async () => {
+ try {
+ setApiKeyErrorText("");
+ setServerIdErrorText("");
+
+ if (apiKey.length === 0) {
+ setApiKeyErrorText("Access Token cannot be blank");
+ return;
+ }
+
+ if (serverId.length === 0) {
+ setServerIdErrorText("Server Id cannot be blank");
+ return;
+ }
+
+ setIsLoading(true);
+
+ const integrationAuth = await saveIntegrationAccessToken({
+ workspaceId: localStorage.getItem("projectData.id"),
+ integration: "laravel-forge",
+ accessId: serverId,
+ accessToken: apiKey,
+ url: null,
+ namespace: null
+ });
+
+ setIsLoading(false);
+
+ router.push(`/integrations/laravel-forge/create?integrationAuthId=${integrationAuth._id}`);
+ } catch (err) {
+ console.error(err);
+ }
+ };
+
+ return (
+
+
+ Laravel Forge Integration
+
+ setApiKey(e.target.value)} />
+
+
+ setServerId(e.target.value)} />
+
+
+ Connect to Laravel Forge
+
+
+
+ );
+}
+
+LaravelForgeCreateIntegrationPage.requireAuth = true;
diff --git a/frontend/src/pages/integrations/laravel-forge/create.tsx b/frontend/src/pages/integrations/laravel-forge/create.tsx
new file mode 100644
index 000000000..acfeb6932
--- /dev/null
+++ b/frontend/src/pages/integrations/laravel-forge/create.tsx
@@ -0,0 +1,155 @@
+import { useEffect, useState } from "react";
+import { useRouter } from "next/router";
+import queryString from "query-string";
+
+import {
+ Button,
+ Card,
+ CardTitle,
+ FormControl,
+ Input,
+ Select,
+ SelectItem
+} from "../../../components/v2";
+import {
+ useGetIntegrationAuthApps,
+ useGetIntegrationAuthById
+} from "../../../hooks/api/integrationAuth";
+import { useGetWorkspaceById } from "../../../hooks/api/workspace";
+import createIntegration from "../../api/integrations/createIntegration";
+
+export default function LaravelForgeCreateIntegrationPage() {
+ const router = useRouter();
+
+ 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({
+ integrationAuthId: (integrationAuthId as string) ?? ""
+ });
+
+ const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState("");
+ const [targetApp, setTargetApp] = useState("");
+ const [secretPath, setSecretPath] = useState("/");
+ const [isLoading, setIsLoading] = useState(false);
+
+ useEffect(() => {
+ if (workspace) {
+ setSelectedSourceEnvironment(workspace.environments[0].slug);
+ }
+ }, [workspace]);
+
+ useEffect(() => {
+ if (integrationAuthApps) {
+ if (integrationAuthApps.length > 0) {
+ setTargetApp(integrationAuthApps[0].name);
+ } else {
+ setTargetApp("none");
+ }
+ }
+ }, [integrationAuthApps]);
+
+ const handleButtonClick = async () => {
+ try {
+ if (!integrationAuth?._id) return;
+
+ setIsLoading(true);
+
+ await createIntegration({
+ integrationAuthId: integrationAuth?._id,
+ isActive: true,
+ app: targetApp,
+ appId:
+ integrationAuthApps?.find((integrationAuthApp) => integrationAuthApp.name === targetApp)
+ ?.appId ?? null,
+ sourceEnvironment: selectedSourceEnvironment,
+ targetEnvironment: null,
+ targetEnvironmentId: null,
+ targetService: null,
+ targetServiceId: null,
+ owner: null,
+ path: null,
+ region: null,
+ secretPath
+ });
+
+ setIsLoading(false);
+
+ router.push(`/integrations/${localStorage.getItem("projectData.id")}`);
+ } catch (err) {
+ console.error(err);
+ }
+ };
+
+ return integrationAuth &&
+ workspace &&
+ selectedSourceEnvironment &&
+ integrationAuthApps &&
+ targetApp ? (
+
+
+ Laravel Forge Integration
+
+ setSelectedSourceEnvironment(val)}
+ className="w-full border border-mineshaft-500"
+ >
+ {workspace?.environments.map((sourceEnvironment) => (
+
+ {sourceEnvironment.name}
+
+ ))}
+
+
+
+ setSecretPath(evt.target.value)}
+ placeholder="Provide a path, default is /"
+ />
+
+
+ setTargetApp(val)}
+ className="w-full border border-mineshaft-500"
+ isDisabled={integrationAuthApps.length === 0}
+ >
+ {integrationAuthApps.length > 0 ? (
+ integrationAuthApps.map((integrationAuthApp) => (
+
+ {integrationAuthApp.name}
+
+ ))
+ ) : (
+
+ No sites found
+
+ )}
+
+
+
+ Create Integration
+
+
+
+ ) : (
+
+ );
+}
+
+LaravelForgeCreateIntegrationPage.requireAuth = true;
diff --git a/frontend/src/views/IntegrationsPage/IntegrationPage.utils.tsx b/frontend/src/views/IntegrationsPage/IntegrationPage.utils.tsx
index fc60ead6d..fd82d1fa7 100644
--- a/frontend/src/views/IntegrationsPage/IntegrationPage.utils.tsx
+++ b/frontend/src/views/IntegrationsPage/IntegrationPage.utils.tsx
@@ -71,6 +71,9 @@ export const redirectForProviderAuth = (integrationOption: TCloudIntegration) =>
case "circleci":
link = `${window.location.origin}/integrations/circleci/authorize`;
break;
+ case "laravel-forge":
+ link = `${window.location.origin}/integrations/laravel-forge/authorize`;
+ break;
case "travisci":
link = `${window.location.origin}/integrations/travisci/authorize`;
break;
diff --git a/frontend/src/views/Settings/BillingSettingsPage/components/BillingCloudTab/PreviewSection.tsx b/frontend/src/views/Settings/BillingSettingsPage/components/BillingCloudTab/PreviewSection.tsx
index 9d7bd0374..23a4be24d 100644
--- a/frontend/src/views/Settings/BillingSettingsPage/components/BillingCloudTab/PreviewSection.tsx
+++ b/frontend/src/views/Settings/BillingSettingsPage/components/BillingCloudTab/PreviewSection.tsx
@@ -62,8 +62,8 @@ export const PreviewSection = () => {
Current plan
-
- {formatPlanSlug(subscription.slug)}
+
+ {`${formatPlanSlug(subscription.slug)} ${subscription.status === "trialing" ? "(Trial)" : ""}`}
{
Price
- {`${formatAmount(data.amount)} / ${data.interval}`}
+ {subscription.status === "trialing" ? "$0.00 / month" : `${formatAmount(data.amount)} / ${data.interval}`}
diff --git a/frontend/src/views/Settings/PersonalSettingsPage/PersonalSecurityTab/PersonalSecurityTab.tsx b/frontend/src/views/Settings/PersonalSettingsPage/PersonalSecurityTab/PersonalSecurityTab.tsx
index b7f4511fb..4c0b21454 100644
--- a/frontend/src/views/Settings/PersonalSettingsPage/PersonalSecurityTab/PersonalSecurityTab.tsx
+++ b/frontend/src/views/Settings/PersonalSettingsPage/PersonalSecurityTab/PersonalSecurityTab.tsx
@@ -1,3 +1,4 @@
+import { ChangeLanguageSection } from "../ChangeLanguageSection";
import { ChangePasswordSection } from "../ChangePasswordSection";
import { EmergencyKitSection } from "../EmergencyKitSection";
import { SecuritySection } from "../SecuritySection";
@@ -6,6 +7,7 @@ import { SessionsSection } from "../SessionsSection";
export const PersonalSecurityTab = () => {
return (
+
diff --git a/frontend/src/views/Settings/PersonalSettingsPage/PersonalTabGroup/PersonalTabGroup.tsx b/frontend/src/views/Settings/PersonalSettingsPage/PersonalTabGroup/PersonalTabGroup.tsx
index 1a0c38e94..d64283bd5 100644
--- a/frontend/src/views/Settings/PersonalSettingsPage/PersonalTabGroup/PersonalTabGroup.tsx
+++ b/frontend/src/views/Settings/PersonalSettingsPage/PersonalTabGroup/PersonalTabGroup.tsx
@@ -5,7 +5,7 @@ import { PersonalAPIKeyTab } from "../PersonalAPIKeyTab";
import { PersonalSecurityTab } from "../PersonalSecurityTab";
const tabs = [
- { name: "Security", key: "tab-account-security" },
+ { name: "General", key: "tab-account-security" },
{ name: "API Keys", key: "tab-account-api-keys" }
];
diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenSection/AddServiceTokenModal.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenSection/AddServiceTokenModal.tsx
index 338738ea2..a44db233b 100644
--- a/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenSection/AddServiceTokenModal.tsx
+++ b/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenSection/AddServiceTokenModal.tsx
@@ -1,9 +1,9 @@
import crypto from "crypto";
import { useEffect, useState } from "react";
-import { Controller, useForm } from "react-hook-form";
+import { Controller, useFieldArray, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
-import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
+import { faCheck, faCopy, faPlus, faTrashCan } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
@@ -27,10 +27,7 @@ import {
} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { useToggle } from "@app/hooks";
-import {
- useCreateServiceToken,
- useGetUserWsKey
-} from "@app/hooks/api";
+import { useCreateServiceToken, useGetUserWsKey } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
const apiTokenExpiry = [
@@ -44,8 +41,23 @@ const apiTokenExpiry = [
const schema = yup.object({
name: yup.string().max(100).required().label("Service Token Name"),
- environment: yup.string().max(50).required().label("Environment"),
- secretPath: yup.string().required().default("/").label("Secret Path"),
+ scopes: yup
+ .array(
+ yup.object({
+ environment: yup.string().max(50).required().label("Environment"),
+ secretPath: yup
+ .string()
+ .required()
+ .default("/")
+ .label("Secret Path")
+ .transform((val) =>
+ typeof val === "string" && val.at(-1) === "/" && val.length > 1 ? val.slice(0, -1) : val
+ )
+ })
+ )
+ .min(1)
+ .required()
+ .label("Scope"),
expiresIn: yup.string().optional().label("Service Token Expiration"),
permissions: yup
.object()
@@ -60,284 +72,301 @@ const schema = yup.object({
export type FormData = yup.InferType
;
type Props = {
- popUp: UsePopUpState<["createAPIToken"]>;
- handlePopUpToggle: (popUpName: keyof UsePopUpState<["createAPIToken"]>, state?: boolean) => void;
+ popUp: UsePopUpState<["createAPIToken"]>;
+ handlePopUpToggle: (popUpName: keyof UsePopUpState<["createAPIToken"]>, state?: boolean) => void;
};
-export const AddServiceTokenModal = ({
- popUp,
- handlePopUpToggle
-}: Props) => {
- const { t } = useTranslation();
- const { createNotification } = useNotificationContext();
- const { currentWorkspace } = useWorkspace();
- const {
- control,
- reset,
- handleSubmit,
- formState: { isSubmitting }
- } = useForm({
- resolver: yupResolver(schema)
- });
+export const AddServiceTokenModal = ({ popUp, handlePopUpToggle }: Props) => {
+ const { t } = useTranslation();
+ const { createNotification } = useNotificationContext();
+ const { currentWorkspace } = useWorkspace();
+ const {
+ control,
+ reset,
+ handleSubmit,
+ formState: { isSubmitting }
+ } = useForm({
+ resolver: yupResolver(schema),
+ defaultValues: {
+ scopes: [{ secretPath: "/", environment: currentWorkspace?.environments?.[0]?.slug }]
+ }
+ });
- const [newToken, setToken] = useState("");
- const [isTokenCopied, setIsTokenCopied] = useToggle(false);
+ const { fields: tokenScopes, append, remove } = useFieldArray({ control, name: "scopes" });
- const { data: latestFileKey } = useGetUserWsKey(currentWorkspace?._id ?? "");
- const createServiceToken = useCreateServiceToken();
- const hasServiceToken = Boolean(newToken);
+ const [newToken, setToken] = useState("");
+ const [isTokenCopied, setIsTokenCopied] = useToggle(false);
- useEffect(() => {
- let timer: NodeJS.Timeout;
- if (isTokenCopied) {
- timer = setTimeout(() => setIsTokenCopied.off(), 2000);
- }
+ const { data: latestFileKey } = useGetUserWsKey(currentWorkspace?._id ?? "");
+ const createServiceToken = useCreateServiceToken();
+ const hasServiceToken = Boolean(newToken);
- return () => clearTimeout(timer);
- }, [isTokenCopied]);
+ useEffect(() => {
+ let timer: NodeJS.Timeout;
+ if (isTokenCopied) {
+ timer = setTimeout(() => setIsTokenCopied.off(), 2000);
+ }
- const copyTokenToClipboard = () => {
- navigator.clipboard.writeText(newToken);
- setIsTokenCopied.on();
- };
+ return () => clearTimeout(timer);
+ }, [isTokenCopied]);
- const onFormSubmit = async ({
+ const copyTokenToClipboard = () => {
+ navigator.clipboard.writeText(newToken);
+ setIsTokenCopied.on();
+ };
+
+ const onFormSubmit = async ({ name, scopes, expiresIn, permissions }: FormData) => {
+ try {
+ if (!currentWorkspace?._id) return;
+ if (!latestFileKey) return;
+
+ const key = decryptAssymmetric({
+ ciphertext: latestFileKey.encryptedKey,
+ nonce: latestFileKey.nonce,
+ publicKey: latestFileKey.sender.publicKey,
+ privateKey: localStorage.getItem("PRIVATE_KEY") as string
+ });
+
+ const randomBytes = crypto.randomBytes(16).toString("hex");
+
+ const { ciphertext, iv, tag } = encryptSymmetric({
+ plaintext: key,
+ key: randomBytes
+ });
+
+ const { serviceToken } = await createServiceToken.mutateAsync({
+ encryptedKey: ciphertext,
+ iv,
+ tag,
+ scopes,
+ expiresIn: Number(expiresIn),
name,
- environment,
- secretPath,
- expiresIn,
- permissions
- }: FormData) => {
- try {
- if (!currentWorkspace?._id) return;
- if (!latestFileKey) return;
+ workspaceId: currentWorkspace._id,
+ randomBytes,
+ permissions: Object.entries(permissions)
+ .filter(([, permissionsValue]) => permissionsValue)
+ .map(([permissionsKey]) => permissionsKey)
+ });
- const key = decryptAssymmetric({
- ciphertext: latestFileKey.encryptedKey,
- nonce: latestFileKey.nonce,
- publicKey: latestFileKey.sender.publicKey,
- privateKey: localStorage.getItem("PRIVATE_KEY") as string
- });
+ setToken(serviceToken);
+ createNotification({
+ text: "Successfully created a service token",
+ type: "success"
+ });
+ } catch (err) {
+ console.error(err);
+ createNotification({
+ text: "Failed to create a service token",
+ type: "error"
+ });
+ }
+ };
- const randomBytes = crypto.randomBytes(16).toString("hex");
-
- const { ciphertext, iv, tag } = encryptSymmetric({
- plaintext: key,
- key: randomBytes
- });
-
- const { serviceToken } = await createServiceToken.mutateAsync({
- encryptedKey: ciphertext,
- iv,
- tag,
- environment,
- secretPath,
- expiresIn: Number(expiresIn),
- name,
- workspaceId: currentWorkspace._id,
- randomBytes,
- permissions: Object.entries(permissions)
- .filter(([, permissionsValue]) => permissionsValue)
- .map(([permissionsKey]) => permissionsKey)
- });
-
- setToken(serviceToken);
-
- createNotification({
- text: "Successfully created a service token",
- type: "success"
- });
-
- } catch (err) {
- console.error(err);
- createNotification({
- text: "Failed to create a service token",
- type: "error"
- });
+ return (
+ {
+ handlePopUpToggle("createAPIToken", open);
+ reset();
+ setToken("");
+ }}
+ >
+ {
- handlePopUpToggle("createAPIToken", open);
- reset();
- setToken("");
- }}
- >
-
- {!hasServiceToken ? (
-
- ) : (
-
-
{newToken}
-
-
-
- {t("common.click-to-copy")}
-
-
-
+ subTitle={t("section.token.add-dialog.description") as string}
+ >
+ {!hasServiceToken ? (
+
-
- );
-}
\ No newline at end of file
+ />
+ {tokenScopes.map(({ id }, index) => (
+
+ (
+
+ onChange(e)}
+ className="w-full"
+ >
+ {currentWorkspace?.environments.map(({ name, slug }) => (
+
+ {name}
+
+ ))}
+
+
+ )}
+ />
+ (
+
+
+
+ )}
+ />
+ remove(index)}
+ >
+
+
+
+ ))}
+
+
+ append({
+ environment: currentWorkspace?.environments?.[0]?.slug || "",
+ secretPath: ""
+ })
+ }
+ leftIcon={ }
+ size="xs"
+ >
+ Add Scope
+
+
+ (
+
+ onChange(e)}
+ className="w-full"
+ >
+ {apiTokenExpiry.map(({ label, value }) => (
+
+ {label}
+
+ ))}
+
+
+ )}
+ />
+ {
+ const options = [
+ {
+ label: "Read (default)",
+ value: "read"
+ },
+ {
+ label: "Write (optional)",
+ value: "write"
+ }
+ ];
+
+ return (
+
+ <>
+ {options.map(({ label, value: optionValue }) => {
+ return (
+ {
+ onChange({
+ ...value,
+ [optionValue]: state
+ });
+ }}
+ >
+ {label}
+
+ );
+ })}
+ >
+
+ );
+ }}
+ />
+
+
+ Create
+
+
+
+ Cancel
+
+
+
+
+ ) : (
+
+
{newToken}
+
+
+
+ {t("common.click-to-copy")}
+
+
+
+ )}
+
+
+ );
+};
diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenSection/ServiceTokenSection.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenSection/ServiceTokenSection.tsx
index fd31870f1..376328673 100644
--- a/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenSection/ServiceTokenSection.tsx
+++ b/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenSection/ServiceTokenSection.tsx
@@ -3,14 +3,9 @@ import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
-import {
- Button,
- DeleteActionModal,
-} from "@app/components/v2";
+import { Button, DeleteActionModal } from "@app/components/v2";
import { usePopUp } from "@app/hooks";
-import {
- useDeleteServiceToken
-} from "@app/hooks/api";
+import { useDeleteServiceToken } from "@app/hooks/api";
import { AddServiceTokenModal } from "./AddServiceTokenModal";
import { ServiceTokenTable } from "./ServiceTokenTable";
@@ -29,7 +24,9 @@ export const ServiceTokenSection = () => {
const onDeleteApproved = async () => {
try {
- deleteServiceToken.mutateAsync((popUp?.deleteAPITokenConfirmation?.data as DeleteModalData)?.id);
+ deleteServiceToken.mutateAsync(
+ (popUp?.deleteAPITokenConfirmation?.data as DeleteModalData)?.id
+ );
createNotification({
text: "Successfully deleted service token",
type: "success"
@@ -46,32 +43,29 @@ export const ServiceTokenSection = () => {
};
return (
-
-
-
{t("section.token.service-tokens")}
-
}
- onClick={() => {
- handlePopUpOpen("createAPIToken");
- }}
- >
- Create token
-
+
+
+
+ {t("section.token.service-tokens")}
+
+
}
+ onClick={() => {
+ handlePopUpOpen("createAPIToken");
+ }}
+ >
+ Create token
+
-
{t("section.token.service-tokens-description")}
-
-
+
{t("section.token.service-tokens-description")}
+
+
handlePopUpToggle("deleteAPITokenConfirmation", isOpen)}
deleteKey={(popUp?.deleteAPITokenConfirmation?.data as DeleteModalData)?.name}
onClose={() => handlePopUpClose("deleteAPITokenConfirmation")}
diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenSection/ServiceTokenTable.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenSection/ServiceTokenTable.tsx
index 69dab23df..ce39abde2 100644
--- a/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenSection/ServiceTokenTable.tsx
+++ b/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenSection/ServiceTokenTable.tsx
@@ -1,4 +1,4 @@
-import { faKey, faTrashCan } from "@fortawesome/free-solid-svg-icons";
+import { faFolder, faKey, faTrashCan } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
@@ -18,71 +18,82 @@ import { useGetUserWsServiceTokens } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = {
- handlePopUpOpen: (
- popUpName: keyof UsePopUpState<["deleteAPITokenConfirmation"]>,
- {
- name,
- id
- }: {
- name: string;
- id: string;
- }
- ) => void;
+ handlePopUpOpen: (
+ popUpName: keyof UsePopUpState<["deleteAPITokenConfirmation"]>,
+ {
+ name,
+ id
+ }: {
+ name: string;
+ id: string;
+ }
+ ) => void;
};
-export const ServiceTokenTable = ({
- handlePopUpOpen
-}: Props) => {
- const { currentWorkspace } = useWorkspace();
- const { data, isLoading } = useGetUserWsServiceTokens({
- workspaceID: currentWorkspace?._id || ""
- });
+export const ServiceTokenTable = ({ handlePopUpOpen }: Props) => {
+ const { currentWorkspace } = useWorkspace();
+ const { data, isLoading } = useGetUserWsServiceTokens({
+ workspaceID: currentWorkspace?._id || ""
+ });
- return (
-
-
-
-
- Token Name
- Environment
- Secret Path
- Valid Until
-
-
-
-
- {isLoading && }
- {!isLoading && data && data.map((row) => (
-
- {row.name}
- {row.environment}
- {row.secretPath}
- {row.expiresAt && new Date(row.expiresAt).toUTCString()}
-
-
- handlePopUpOpen("deleteAPITokenConfirmation", {
- name: row.name,
- id: row._id
- })
- }
- colorSchema="danger"
- ariaLabel="delete"
- >
-
-
-
-
+ return (
+
+
+
+
+ Token Name
+ Envrionment - Secret Path
+ Valid Until
+
+
+
+
+ {isLoading && }
+ {!isLoading &&
+ data &&
+ data.map((row) => (
+
+ {row.name}
+
+
+ {row?.scopes.map(({ secretPath, environment }) => (
+
+
{environment}
+
+
{secretPath}
+
))}
- {!isLoading && data && data?.length === 0 && (
-
-
-
-
-
- )}
-
-
-
- );
-}
\ No newline at end of file
+
+
+ {row.expiresAt && new Date(row.expiresAt).toUTCString()}
+
+
+ handlePopUpOpen("deleteAPITokenConfirmation", {
+ name: row.name,
+ id: row._id
+ })
+ }
+ colorSchema="danger"
+ ariaLabel="delete"
+ >
+
+
+
+
+ ))}
+ {!isLoading && data && data?.length === 0 && (
+
+
+
+
+
+ )}
+
+
+
+ );
+};
diff --git a/helm-charts/secrets-operator/Chart.yaml b/helm-charts/secrets-operator/Chart.yaml
index c3e0073b5..d44467dec 100644
--- a/helm-charts/secrets-operator/Chart.yaml
+++ b/helm-charts/secrets-operator/Chart.yaml
@@ -13,9 +13,9 @@ type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
-version: 0.1.6
+version: 0.2.0
# This is the version number of the application being deployed. This version number should be
# 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.8"
+appVersion: "0.2.0"
diff --git a/helm-charts/secrets-operator/templates/infisicalsecret-crd.yaml b/helm-charts/secrets-operator/templates/infisicalsecret-crd.yaml
index ceeac6709..51dd18a41 100644
--- a/helm-charts/secrets-operator/templates/infisicalsecret-crd.yaml
+++ b/helm-charts/secrets-operator/templates/infisicalsecret-crd.yaml
@@ -63,6 +63,16 @@ spec:
type: object
serviceToken:
properties:
+ secretsScope:
+ properties:
+ envSlug:
+ type: string
+ secretsPath:
+ type: string
+ required:
+ - envSlug
+ - secretsPath
+ type: object
serviceTokenSecretReference:
properties:
secretName:
@@ -77,6 +87,7 @@ spec:
- secretNamespace
type: object
required:
+ - secretsScope
- serviceTokenSecretReference
type: object
type: object
diff --git a/k8-operator/api/v1alpha1/infisicalsecret_types.go b/k8-operator/api/v1alpha1/infisicalsecret_types.go
index 203394417..3b61bd215 100644
--- a/k8-operator/api/v1alpha1/infisicalsecret_types.go
+++ b/k8-operator/api/v1alpha1/infisicalsecret_types.go
@@ -4,8 +4,19 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
+type Authentication struct {
+ // +kubebuilder:validation:Optional
+ ServiceAccount ServiceAccountDetails `json:"serviceAccount"`
+ // +kubebuilder:validation:Optional
+ ServiceToken ServiceTokenDetails `json:"serviceToken"`
+}
+
type ServiceTokenDetails struct {
+ // +kubebuilder:validation:Required
ServiceTokenSecretReference KubeSecretReference `json:"serviceTokenSecretReference"`
+
+ // +kubebuilder:validation:Required
+ SecretsScope SecretScopeInWorkspace `json:"secretsScope"`
}
type ServiceAccountDetails struct {
@@ -14,11 +25,12 @@ type ServiceAccountDetails struct {
EnvironmentName string `json:"environmentName"`
}
-type Authentication struct {
- // +kubebuilder:validation:Optional
- ServiceAccount ServiceAccountDetails `json:"serviceAccount"`
- // +kubebuilder:validation:Optional
- ServiceToken ServiceTokenDetails `json:"serviceToken"`
+type SecretScopeInWorkspace struct {
+ // +kubebuilder:validation:Required
+ SecretsPath string `json:"secretsPath"`
+
+ // +kubebuilder:validation:Required
+ EnvSlug string `json:"envSlug"`
}
type KubeSecretReference struct {
diff --git a/k8-operator/api/v1alpha1/zz_generated.deepcopy.go b/k8-operator/api/v1alpha1/zz_generated.deepcopy.go
index 97c0f4fc7..01000431c 100644
--- a/k8-operator/api/v1alpha1/zz_generated.deepcopy.go
+++ b/k8-operator/api/v1alpha1/zz_generated.deepcopy.go
@@ -157,6 +157,21 @@ func (in *KubeSecretReference) DeepCopy() *KubeSecretReference {
return out
}
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *SecretScopeInWorkspace) DeepCopyInto(out *SecretScopeInWorkspace) {
+ *out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretScopeInWorkspace.
+func (in *SecretScopeInWorkspace) DeepCopy() *SecretScopeInWorkspace {
+ if in == nil {
+ return nil
+ }
+ out := new(SecretScopeInWorkspace)
+ in.DeepCopyInto(out)
+ return out
+}
+
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ServiceAccountDetails) DeepCopyInto(out *ServiceAccountDetails) {
*out = *in
@@ -177,6 +192,7 @@ func (in *ServiceAccountDetails) DeepCopy() *ServiceAccountDetails {
func (in *ServiceTokenDetails) DeepCopyInto(out *ServiceTokenDetails) {
*out = *in
out.ServiceTokenSecretReference = in.ServiceTokenSecretReference
+ out.SecretsScope = in.SecretsScope
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceTokenDetails.
diff --git a/k8-operator/config/crd/bases/secrets.infisical.com_infisicalsecrets.yaml b/k8-operator/config/crd/bases/secrets.infisical.com_infisicalsecrets.yaml
index f47efb061..07433f781 100644
--- a/k8-operator/config/crd/bases/secrets.infisical.com_infisicalsecrets.yaml
+++ b/k8-operator/config/crd/bases/secrets.infisical.com_infisicalsecrets.yaml
@@ -63,6 +63,16 @@ spec:
type: object
serviceToken:
properties:
+ secretsScope:
+ properties:
+ envSlug:
+ type: string
+ secretsPath:
+ type: string
+ required:
+ - envSlug
+ - secretsPath
+ type: object
serviceTokenSecretReference:
properties:
secretName:
@@ -77,6 +87,7 @@ spec:
- secretNamespace
type: object
required:
+ - secretsScope
- serviceTokenSecretReference
type: object
type: object
diff --git a/k8-operator/config/samples/sample.yaml b/k8-operator/config/samples/sample.yaml
index 971df9f7c..4c5059d9f 100644
--- a/k8-operator/config/samples/sample.yaml
+++ b/k8-operator/config/samples/sample.yaml
@@ -3,8 +3,8 @@ kind: InfisicalSecret
metadata:
name: infisicalsecret-sample
spec:
- hostAPI: http://localhost:7070/api
- resyncInterval: 60
+ hostAPI: http://localhost:8764/api
+ resyncInterval: 10
authentication:
serviceAccount:
serviceAccountSecretReference:
@@ -16,10 +16,13 @@ spec:
serviceTokenSecretReference:
secretName: service-token
secretNamespace: default
+ secretsScope:
+ envSlug: dev
+ secretsPath: "/"
managedSecretReference:
secretName: managed-secret
secretNamespace: default
- # To be depreciated soon
- tokenSecretReference:
- secretName: service-token
- secretNamespace: default
+ # # To be depreciated soon
+ # tokenSecretReference:
+ # secretName: service-token
+ # secretNamespace: default
diff --git a/k8-operator/controllers/infisicalsecret_helper.go b/k8-operator/controllers/infisicalsecret_helper.go
index 32216e2e3..509de94ac 100644
--- a/k8-operator/controllers/infisicalsecret_helper.go
+++ b/k8-operator/controllers/infisicalsecret_helper.go
@@ -219,7 +219,10 @@ func (r *InfisicalSecretReconciler) ReconcileInfisicalSecret(ctx context.Context
fmt.Println("ReconcileInfisicalSecret: Fetched secrets via service account")
} else if infisicalToken != "" {
- plainTextSecretsFromApi, fullEncryptedSecretsResponse, err = util.GetPlainTextSecretsViaServiceToken(infisicalToken, secretVersionBasedOnETag)
+ envSlug := infisicalSecret.Spec.Authentication.ServiceToken.SecretsScope.EnvSlug
+ secretsPath := infisicalSecret.Spec.Authentication.ServiceToken.SecretsScope.SecretsPath
+
+ plainTextSecretsFromApi, fullEncryptedSecretsResponse, err = util.GetPlainTextSecretsViaServiceToken(infisicalToken, secretVersionBasedOnETag, envSlug, secretsPath)
if err != nil {
return fmt.Errorf("\nfailed to get secrets because [err=%v]", err)
}
diff --git a/k8-operator/kubectl-install/install-secrets-operator.yaml b/k8-operator/kubectl-install/install-secrets-operator.yaml
index f362bcfa1..dad6dcf3a 100644
--- a/k8-operator/kubectl-install/install-secrets-operator.yaml
+++ b/k8-operator/kubectl-install/install-secrets-operator.yaml
@@ -70,6 +70,16 @@ spec:
type: object
serviceToken:
properties:
+ secretsScope:
+ properties:
+ envSlug:
+ type: string
+ secretsPath:
+ type: string
+ required:
+ - envSlug
+ - secretsPath
+ type: object
serviceTokenSecretReference:
properties:
secretName:
@@ -83,6 +93,7 @@ spec:
- secretNamespace
type: object
required:
+ - secretsScope
- serviceTokenSecretReference
type: object
type: object
diff --git a/k8-operator/packages/util/secrets.go b/k8-operator/packages/util/secrets.go
index 5087cd03a..28ba47253 100644
--- a/k8-operator/packages/util/secrets.go
+++ b/k8-operator/packages/util/secrets.go
@@ -3,6 +3,8 @@ package util
import (
"encoding/base64"
"fmt"
+ "path"
+ "regexp"
"strings"
"github.com/Infisical/infisical/k8-operator/packages/api"
@@ -48,7 +50,7 @@ func GetServiceTokenDetails(infisicalToken string) (api.GetServiceTokenDetailsRe
return serviceTokenDetails, nil
}
-func GetPlainTextSecretsViaServiceToken(fullServiceToken string, etag string) ([]model.SingleEnvironmentVariable, api.GetEncryptedSecretsV3Response, error) {
+func GetPlainTextSecretsViaServiceToken(fullServiceToken string, etag string, envSlug string, secretPath string) ([]model.SingleEnvironmentVariable, api.GetEncryptedSecretsV3Response, error) {
serviceTokenParts := strings.SplitN(fullServiceToken, ".", 4)
if len(serviceTokenParts) < 4 {
return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("invalid service token entered. Please double check your service token and try again")
@@ -68,9 +70,9 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string, etag string) ([
encryptedSecretsResponse, err := api.CallGetSecretsV3(httpClient, api.GetEncryptedSecretsV3Request{
WorkspaceId: serviceTokenDetails.Workspace,
- Environment: serviceTokenDetails.Environment,
+ Environment: envSlug,
ETag: etag,
- SecretPath: serviceTokenDetails.SecretPath,
+ SecretPath: secretPath,
})
if err != nil {
@@ -92,7 +94,10 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string, etag string) ([
return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("unable to decrypt your secrets [err=%v]", err)
}
- return plainTextSecrets, encryptedSecretsResponse, nil
+ // expand secrets that are referenced
+ expandedSecrets := ExpandSecrets(plainTextSecrets, fullServiceToken)
+
+ return expandedSecrets, encryptedSecretsResponse, nil
}
// Fetches plaintext secrets from an API endpoint using a service account.
@@ -252,3 +257,104 @@ func GetPlainTextSecrets(key []byte, encryptedSecretsResponse api.GetEncryptedSe
return plainTextSecrets, nil
}
+
+var secRefRegex = regexp.MustCompile(`\${([^\}]*)}`)
+
+func recursivelyExpandSecret(expandedSecs map[string]string, interpolatedSecs map[string]string, crossSecRefFetch func(env string, path []string, key string) string, key string) string {
+ if v, ok := expandedSecs[key]; ok {
+ return v
+ }
+
+ interpolatedVal, ok := interpolatedSecs[key]
+ if !ok {
+ return ""
+ // panic(fmt.Errorf("Could not find referred secret with key name %s", key), "Please check it refers a")
+ }
+
+ refs := secRefRegex.FindAllStringSubmatch(interpolatedVal, -1)
+ for _, val := range refs {
+ // key: "${something}" val: [${something},something]
+ interpolatedExp, interpolationKey := val[0], val[1]
+ ref := strings.Split(interpolationKey, ".")
+
+ // ${KEY1} => [key1]
+ if len(ref) == 1 {
+ val := recursivelyExpandSecret(expandedSecs, interpolatedSecs, crossSecRefFetch, interpolationKey)
+ interpolatedVal = strings.ReplaceAll(interpolatedVal, interpolatedExp, val)
+ continue
+ }
+
+ // cross board reference ${env.folder.key1} => [env folder key1]
+ if len(ref) > 1 {
+ secEnv, tmpSecPath, secKey := ref[0], ref[1:len(ref)-1], ref[len(ref)-1]
+ interpolatedSecs[interpolationKey] = crossSecRefFetch(secEnv, tmpSecPath, secKey) // get the reference value
+ val := recursivelyExpandSecret(expandedSecs, interpolatedSecs, crossSecRefFetch, interpolationKey)
+ interpolatedVal = strings.ReplaceAll(interpolatedVal, interpolatedExp, val)
+ }
+
+ }
+ expandedSecs[key] = interpolatedVal
+ return interpolatedVal
+}
+
+func ExpandSecrets(secrets []model.SingleEnvironmentVariable, infisicalToken string) []model.SingleEnvironmentVariable {
+ expandedSecs := make(map[string]string)
+ interpolatedSecs := make(map[string]string)
+ // map[env.secret-path][keyname]Secret
+ crossEnvRefSecs := make(map[string]map[string]model.SingleEnvironmentVariable) // a cache to hold all cross board reference secrets
+
+ for _, sec := range secrets {
+ // get all references in a secret
+ refs := secRefRegex.FindAllStringSubmatch(sec.Value, -1)
+ // nil means its a secret without reference
+ if refs == nil {
+ expandedSecs[sec.Key] = sec.Value // atomic secrets without any interpolation
+ } else {
+ interpolatedSecs[sec.Key] = sec.Value
+ }
+ }
+
+ for i, sec := range secrets {
+ // already present pick that up
+ if expandedVal, ok := expandedSecs[sec.Key]; ok {
+ secrets[i].Value = expandedVal
+ continue
+ }
+
+ expandedVal := recursivelyExpandSecret(expandedSecs, interpolatedSecs, func(env string, secPaths []string, secKey string) string {
+ secPaths = append([]string{"/"}, secPaths...)
+ secPath := path.Join(secPaths...)
+
+ secPathDot := strings.Join(secPaths, ".")
+ uniqKey := fmt.Sprintf("%s.%s", env, secPathDot)
+
+ if crossRefSec, ok := crossEnvRefSecs[uniqKey]; !ok {
+ // if not in cross reference cache, fetch it from server
+ refSecs, _, err := GetPlainTextSecretsViaServiceToken(infisicalToken, "", env, secPath)
+ if err != nil {
+ fmt.Println("HELLO===>", "MOO", err)
+ // 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")
+ }
+ refSecsByKey := getSecretsByKeys(refSecs)
+ // 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
+ }
+ }, sec.Key)
+
+ secrets[i].Value = expandedVal
+ }
+ return secrets
+}
+
+func getSecretsByKeys(secrets []model.SingleEnvironmentVariable) map[string]model.SingleEnvironmentVariable {
+ secretMapByName := make(map[string]model.SingleEnvironmentVariable, len(secrets))
+
+ for _, secret := range secrets {
+ secretMapByName[secret.Key] = secret
+ }
+
+ return secretMapByName
+}