Merge remote-tracking branch 'origin' into scim

This commit is contained in:
Tuan Dang
2024-02-19 14:41:46 -08:00
18 changed files with 876 additions and 1409 deletions
@@ -28,7 +28,7 @@ jobs:
run: docker build --tag infisical-api . run: docker build --tag infisical-api .
working-directory: backend working-directory: backend
- name: Start postgres and redis - name: Start postgres and redis
run: touch .env && docker-compose -f docker-compose.prod.yml up -d db redis run: touch .env && docker-compose -f docker-compose.dev.yml up -d db redis
- name: Start the server - name: Start the server
run: | run: |
echo "SECRET_SCANNING_GIT_APP_ID=793712" >> .env echo "SECRET_SCANNING_GIT_APP_ID=793712" >> .env
@@ -70,6 +70,6 @@ jobs:
run: oasdiff breaking https://app.infisical.com/api/docs/json http://localhost:4000/api/docs/json --fail-on ERR run: oasdiff breaking https://app.infisical.com/api/docs/json http://localhost:4000/api/docs/json --fail-on ERR
- name: cleanup - name: cleanup
run: | run: |
docker-compose -f "docker-compose.pg.yml" down docker-compose -f "docker-compose.dev.yml" down
docker stop infisical-api docker stop infisical-api
docker remove infisical-api docker remove infisical-api
+401 -1065
View File
File diff suppressed because it is too large Load Diff
+6 -5
View File
@@ -81,7 +81,7 @@
"@fastify/rate-limit": "^9.0.0", "@fastify/rate-limit": "^9.0.0",
"@fastify/session": "^10.7.0", "@fastify/session": "^10.7.0",
"@fastify/swagger": "^8.12.0", "@fastify/swagger": "^8.12.0",
"@fastify/swagger-ui": "^1.10.1", "@fastify/swagger-ui": "^2.1.0",
"@node-saml/passport-saml": "^4.0.4", "@node-saml/passport-saml": "^4.0.4",
"@octokit/rest": "^20.0.2", "@octokit/rest": "^20.0.2",
"@octokit/webhooks-types": "^7.3.1", "@octokit/webhooks-types": "^7.3.1",
@@ -90,8 +90,8 @@
"@ucast/mongo2js": "^1.3.4", "@ucast/mongo2js": "^1.3.4",
"ajv": "^8.12.0", "ajv": "^8.12.0",
"argon2": "^0.31.2", "argon2": "^0.31.2",
"aws-sdk": "^2.1532.0", "aws-sdk": "^2.1545.0",
"axios": "^1.6.2", "axios": "^1.6.4",
"axios-retry": "^4.0.0", "axios-retry": "^4.0.0",
"bcrypt": "^5.1.1", "bcrypt": "^5.1.1",
"bullmq": "^5.1.1", "bullmq": "^5.1.1",
@@ -109,7 +109,8 @@
"mysql2": "^3.6.5", "mysql2": "^3.6.5",
"nanoid": "^5.0.4", "nanoid": "^5.0.4",
"node-cache": "^5.1.2", "node-cache": "^5.1.2",
"nodemailer": "^6.9.7", "nodemailer": "^6.9.9",
"ora": "^7.0.1",
"passport-github": "^1.1.0", "passport-github": "^1.1.0",
"passport-gitlab2": "^5.0.0", "passport-gitlab2": "^5.0.0",
"passport-google-oauth20": "^2.0.0", "passport-google-oauth20": "^2.0.0",
@@ -117,7 +118,7 @@
"picomatch": "^3.0.1", "picomatch": "^3.0.1",
"pino": "^8.16.2", "pino": "^8.16.2",
"posthog-node": "^3.6.0", "posthog-node": "^3.6.0",
"probot": "^12.3.3", "probot": "^13.0.0",
"smee-client": "^2.0.0", "smee-client": "^2.0.0",
"tweetnacl": "^1.0.3", "tweetnacl": "^1.0.3",
"tweetnacl-util": "^0.15.1", "tweetnacl-util": "^0.15.1",
@@ -156,7 +156,8 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => {
const { user, accessToken, refreshToken } = await server.services.signup.completeAccountInvite({ const { user, accessToken, refreshToken } = await server.services.signup.completeAccountInvite({
...req.body, ...req.body,
ip: req.realIp, ip: req.realIp,
userAgent userAgent,
authorization: req.headers.authorization as string
}); });
void server.services.telemetry.sendLoopsEvent(user.email, user.firstName || "", user.lastName || ""); void server.services.telemetry.sendLoopsEvent(user.email, user.firstName || "", user.lastName || "");
@@ -212,13 +212,16 @@ export const authSignupServiceFactory = ({
protectedKeyTag, protectedKeyTag,
encryptedPrivateKey, encryptedPrivateKey,
encryptedPrivateKeyIV, encryptedPrivateKeyIV,
encryptedPrivateKeyTag encryptedPrivateKeyTag,
authorization
}: TCompleteAccountInviteDTO) => { }: TCompleteAccountInviteDTO) => {
const user = await userDAL.findUserByEmail(email); const user = await userDAL.findUserByEmail(email);
if (!user || (user && user.isAccepted)) { if (!user || (user && user.isAccepted)) {
throw new Error("Failed to complete account for complete user"); throw new Error("Failed to complete account for complete user");
} }
validateSignUpAuthorization(authorization, user.id);
const [orgMembership] = await orgDAL.findMembership({ const [orgMembership] = await orgDAL.findMembership({
inviteEmail: email, inviteEmail: email,
status: OrgMembershipStatus.Invited status: OrgMembershipStatus.Invited
@@ -34,4 +34,5 @@ export type TCompleteAccountInviteDTO = {
verifier: string; verifier: string;
ip: string; ip: string;
userAgent: string; userAgent: string;
authorization: string;
}; };
@@ -41,13 +41,12 @@ export const fnSecretsFromImports = async ({
environment: importEnv.slug, environment: importEnv.slug,
environmentInfo: importEnv, environmentInfo: importEnv,
folderId: importedFolders?.[i]?.id, folderId: importedFolders?.[i]?.id,
secrets: importedFolders?.[i]?.id // this will ensure for cases when secrets are empty. Could be due to missing folder for a path or when emtpy secrets inside a given path
? importedSecsGroupByFolderId[importedFolders?.[i]?.id as string].map((item) => ({ secrets: (importedSecsGroupByFolderId?.[importedFolders?.[i]?.id as string] || []).map((item) => ({
...item, ...item,
environment: importEnv.slug, environment: importEnv.slug,
workspace: "", // This field should not be used, it's only here to keep the older Python SDK versions backwards compatible with the new Postgres backend. workspace: "", // This field should not be used, it's only here to keep the older Python SDK versions backwards compatible with the new Postgres backend.
_id: item.id // The old Python SDK depends on the _id field being returned. We return this to keep the older Python SDK versions backwards compatible with the new Postgres backend. _id: item.id // The old Python SDK depends on the _id field being returned. We return this to keep the older Python SDK versions backwards compatible with the new Postgres backend.
})) }))
: []
})); }));
}; };
+16 -3
View File
@@ -5,6 +5,19 @@ description: "How to use Infisical for secret management in Ansible"
The documentation for using Infisical to manage secrets in Ansible is currently available [here](https://galaxy.ansible.com/ui/repo/published/infisical/vault/). The documentation for using Infisical to manage secrets in Ansible is currently available [here](https://galaxy.ansible.com/ui/repo/published/infisical/vault/).
<Info> ## Troubleshoot
Have any questions? Join Infisical's [community Slack](https://infisical.com/slack) for quick support.
</Info> <Accordion title="I'm getting a error related to objc[72832]: +[__NSCFConstantString initialize]">
If you get this Python error when you running the lookup plugin:-
```
objc[72832]: +[__NSCFConstantString initialize] may have been in progress in another thread when fork() was called. We cannot safely call it or ignore it in the fork() child process. Crashing instead. Set a breakpoint on objc_initializeAfterForkError to debug.
Fatal Python error: Aborted
```
You will need to add this to your shell environment or ansible wrapper script:-
```
export OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES
```
</Accordion>
+1 -1
View File
@@ -51,7 +51,7 @@ CMD ["infisical", "run", "--", "npm", "run", "start"]
CMD ["infisical", "run", "--command", "npm run start && ..."] CMD ["infisical", "run", "--command", "npm run start && ..."]
``` ```
## Generate an service token ## Generate a service token
Head to your project settings in the Infisical dashboard to generate an [service token](/documentation/platform/token). Head to your project settings in the Infisical dashboard to generate an [service token](/documentation/platform/token).
This service token will allow you to authenticate and fetch secrets from Infisical. This service token will allow you to authenticate and fetch secrets from Infisical.
+2 -2
View File
@@ -18,8 +18,8 @@ Other environment variables are listed below to increase the functionality of yo
Must be a random 32 byte base64 string. Can be generated with `openssl rand -base64 32` Must be a random 32 byte base64 string. Can be generated with `openssl rand -base64 32`
</ParamField> </ParamField>
<ParamField query="MONGO_URL" type="string" default="none" required> <ParamField query="DB_CONNECTION_URI" type="string" default="none" required>
Mongo connection string. *TLS based connection string is not yet supported Postgres database connection string.
</ParamField> </ParamField>
<ParamField query="REDIS_URL" type="string" default="none" required> <ParamField query="REDIS_URL" type="string" default="none" required>
@@ -2,53 +2,79 @@
title: "Docker Compose" title: "Docker Compose"
description: "Run Infisical with Docker Compose template" description: "Run Infisical with Docker Compose template"
--- ---
Install Infisical using Docker compose. This self hosting method contains all of the required components needed
to run a functional instance of Infisical.
<Steps> ## Prerequisites
<Step title="Install Docker on your VM"> - [Docker](https://docs.docker.com/engine/install/)
```bash - [Docker compose](https://docs.docker.com/compose/install/)
# Example in ubuntu
apt-get update
apt-get upgrade
apt install docker-compose
```
</Step>
<Step title="Download required files">
2.1. Run the command below to download the `.env` file template.
```bash
wget -O .env https://raw.githubusercontent.com/Infisical/infisical/main/.env.example
```
2.2. Run the command below to download the docker compose template.
```bash
wget -O docker-compose.yml https://raw.githubusercontent.com/Infisical/infisical/main/docker-compose.yml
```
2.3. Run the command below to download the `nginx` config file.
```bash
mkdir nginx && wget -O ./nginx/default.conf https://raw.githubusercontent.com/Infisical/infisical/main/nginx/default.dev.conf
```
</Step>
<Step title="Update the .env file">
Running Infisical requires a few environment variables to be set.
At minimum, Infisical requires that you set the variables `ENCRYPTION_KEY`, `AUTH_SECRET`, `MONGO_URL`, and `REDIS_URL` which you can read more about [here](/self-hosting/configuration/envars).
Tweak the `.env` accordingly. <Warning>
This Docker Compose configuration is not designed for high-availability production scenarios.
It includes just the essential components needed to set up an Infisical proof of concept (POC).
Additional configuration is required to enhance data redundancy and ensure higher availability for production environments.
</Warning>
```bash ## Verify prerequisites
nano .env To verify that Docker compose and Docker are installed on the machine where you plan to install Infisical, run the following commands.
```
</Step>
<Step title="Start Infisical">
Finally, run the command below to get Infisical up and running (in detached mode).
Check for docker installation
```bash ```bash
docker-compose -f docker-compose.yml up -d docker
``` ```
Your Infisical installation is complete and should be running on port `80` or `http://localhost:80`. Check for docker compose installation
</Step> ```bash
</Steps> docker-compose
```
## Download docker compose file
You can obtain the Infisical docker compose file by using a command-line downloader such as `wget` or `curl`.
If your system doesn't have either of these, you can use a equivalent command that works with your machine.
<Tabs>
<Tab title="curl">
```bash
curl -o docker-compose.prod.yml https://raw.githubusercontent.com/Infisical/infisical/main/docker-compose.prod.yml
```
</Tab>
<Tab title="wget">
```bash
wget -O docker-compose.prod.yml https://raw.githubusercontent.com/Infisical/infisical/main/docker-compose.prod.yml
```
</Tab>
</Tabs>
## Configure instance credentials
Infisical requires a set of credentials used for connecting to dependent services such as Postgres, Redis, etc.
The default credentials can be downloaded using the one of the commands listed below.
<Tabs>
<Tab title="curl">
```bash
curl -o .env https://raw.githubusercontent.com/Infisical/infisical/main/.env.example
```
</Tab>
<Tab title="wget">
```bash
wget -O .env https://raw.githubusercontent.com/Infisical/infisical/main/.env.example
```
</Tab>
</Tabs>
Once downloaded, the credentials file will be saved to your working directly as `.env` file.
View all available configurations [here](/self-hosting/configuration/envars).
<Warning>
The default .env file contains credentials that are intended solely for testing purposes.
For production use, please generate a new `ENCRYPTION_KEY` and `AUTH_SECRET`. Instructions to do so, can be found [here](/self-hosting/configuration/envars)
</Warning>
## Start Infisical
Run the command below to start Infisical and all related services.
```bash
docker-compose -f docker-compose.prod.yml up
```
Your Infisical instance should now be running on port `80`. To access your instance, visit `http://localhost:80`.
+6 -6
View File
@@ -1,5 +1,5 @@
{ {
"name": "frontend", "name": "npm-proj-1708142396879-0.7491636790514078NCvb8i",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
@@ -94,7 +94,7 @@
"yaml": "^2.2.2", "yaml": "^2.2.2",
"yup": "^0.32.11", "yup": "^0.32.11",
"zod": "^3.22.3", "zod": "^3.22.3",
"zustand": "^4.4.1" "zustand": "^4.5.0"
}, },
"devDependencies": { "devDependencies": {
"@storybook/addon-essentials": "^7.5.2", "@storybook/addon-essentials": "^7.5.2",
@@ -23536,9 +23536,9 @@
} }
}, },
"node_modules/zustand": { "node_modules/zustand": {
"version": "4.4.7", "version": "4.5.0",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.4.7.tgz", "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.0.tgz",
"integrity": "sha512-QFJWJMdlETcI69paJwhSMJz7PPWjVP8Sjhclxmxmxv/RYI7ZOvR5BHX+ktH0we9gTWQMxcne8q1OY8xxz604gw==", "integrity": "sha512-zlVFqS5TQ21nwijjhJlx4f9iGrXSL0o/+Dpy4txAP22miJ8Ti6c1Ol1RLNN98BMib83lmDH/2KmLwaNXpjrO1A==",
"dependencies": { "dependencies": {
"use-sync-external-store": "1.2.0" "use-sync-external-store": "1.2.0"
}, },
@@ -23547,7 +23547,7 @@
}, },
"peerDependencies": { "peerDependencies": {
"@types/react": ">=16.8", "@types/react": ">=16.8",
"immer": ">=9.0", "immer": ">=9.0.6",
"react": ">=16.8" "react": ">=16.8"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {
+1 -1
View File
@@ -102,7 +102,7 @@
"yaml": "^2.2.2", "yaml": "^2.2.2",
"yup": "^0.32.11", "yup": "^0.32.11",
"zod": "^3.22.3", "zod": "^3.22.3",
"zustand": "^4.4.1" "zustand": "^4.5.0"
}, },
"devDependencies": { "devDependencies": {
"@storybook/addon-essentials": "^7.5.2", "@storybook/addon-essentials": "^7.5.2",
@@ -1,6 +1,6 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
import { faClock } from "@fortawesome/free-solid-svg-icons"; import { faClock, faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
@@ -9,20 +9,28 @@ import { z } from "zod";
import { Button, FormControl, Input, Modal, ModalContent, TextArea } from "@app/components/v2"; import { Button, FormControl, Input, Modal, ModalContent, TextArea } from "@app/components/v2";
const ReminderFormSchema = z.object({ const ReminderFormSchema = z.object({
note: z.string().optional(), note: z.string().optional().nullable(),
days: z days: z
.number() .number()
.min(1, { message: "Must be at least 1 day" }) .min(1, { message: "Must be at least 1 day" })
.max(365, { message: "Must be less than 365 days" }) .max(365, { message: "Must be less than 365 days" })
.nullable()
}); });
export type TReminderFormSchema = z.infer<typeof ReminderFormSchema>; export type TReminderFormSchema = z.infer<typeof ReminderFormSchema>;
interface ReminderFormProps { interface ReminderFormProps {
isOpen: boolean; isOpen: boolean;
repeatDays?: number | null;
note?: string | null;
onOpenChange: (isOpen: boolean, data?: TReminderFormSchema) => void; onOpenChange: (isOpen: boolean, data?: TReminderFormSchema) => void;
} }
export const CreateReminderForm = ({ isOpen, onOpenChange }: ReminderFormProps) => { export const CreateReminderForm = ({
isOpen,
onOpenChange,
repeatDays,
note
}: ReminderFormProps) => {
const { const {
register, register,
control, control,
@@ -31,32 +39,31 @@ export const CreateReminderForm = ({ isOpen, onOpenChange }: ReminderFormProps)
handleSubmit, handleSubmit,
formState: { isSubmitting } formState: { isSubmitting }
} = useForm<TReminderFormSchema>({ } = useForm<TReminderFormSchema>({
defaultValues: {
days: repeatDays || undefined,
note: note || ""
},
resolver: zodResolver(ReminderFormSchema) resolver: zodResolver(ReminderFormSchema)
}); });
const handleFormSubmit = async (data: TReminderFormSchema) => { const handleFormSubmit = async (data: TReminderFormSchema) => {
console.log(data);
onOpenChange(false, data); onOpenChange(false, data);
}; };
useEffect(() => { useEffect(() => {
if (isOpen) { if (isOpen) {
reset(); reset({
days: repeatDays || undefined,
note: note || ""
});
} }
}, [isOpen]); }, [isOpen]);
return ( return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}> <Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent <ModalContent
title="Create secret reminder" title={`${repeatDays ? "Update" : "Create"} reminder`}
// ? QUESTION: Should this specifically say its for secret rotation? subTitle="Set up a reminder for when this secret should be rotated. Everyone with access to this project will be notified when the reminder is triggered."
// ? Or should we be call it something more generic?
subTitle={
<div>
Set up a reminder for when this secret should be rotated. Everyone with access to this
project will be notified when the reminder is triggered.
</div>
}
> >
<form onSubmit={handleSubmit(handleFormSubmit)}> <form onSubmit={handleSubmit(handleFormSubmit)}>
<div className="space-y-2"> <div className="space-y-2">
@@ -68,7 +75,7 @@ export const CreateReminderForm = ({ isOpen, onOpenChange }: ReminderFormProps)
<> <>
<FormControl <FormControl
className="mb-0" className="mb-0"
label="How many days between" label="Reminder Interval (in days)"
isError={Boolean(fieldState.error)} isError={Boolean(fieldState.error)}
errorText={fieldState.error?.message || ""} errorText={fieldState.error?.message || ""}
> >
@@ -76,6 +83,7 @@ export const CreateReminderForm = ({ isOpen, onOpenChange }: ReminderFormProps)
onChange={(el) => setValue("days", parseInt(el.target.value, 10))} onChange={(el) => setValue("days", parseInt(el.target.value, 10))}
type="number" type="number"
placeholder="31" placeholder="31"
value={field.value || undefined}
/> />
</FormControl> </FormControl>
<div <div
@@ -84,7 +92,8 @@ export const CreateReminderForm = ({ isOpen, onOpenChange }: ReminderFormProps)
field.value ? "opacity-60" : "opacity-0" field.value ? "opacity-60" : "opacity-0"
)} )}
> >
Every {field.value > 1 ? `${field.value} days` : "day"} A reminder will be sent every{" "}
{field.value && field.value > 1 ? `${field.value} days` : "day"}
</div> </div>
</> </>
)} )}
@@ -102,17 +111,27 @@ export const CreateReminderForm = ({ isOpen, onOpenChange }: ReminderFormProps)
/> />
</FormControl> </FormControl>
</div> </div>
<div className="mt-7 flex items-center"> <div className="mt-7 flex items-center space-x-4">
<Button <Button
isDisabled={isSubmitting} isDisabled={isSubmitting}
isLoading={isSubmitting} isLoading={isSubmitting}
key="layout-create-project-submit" key="layout-create-project-submit"
className="mr-4" className=""
leftIcon={<FontAwesomeIcon icon={faClock} />} leftIcon={<FontAwesomeIcon icon={faClock} />}
type="submit" type="submit"
> >
Create reminder {repeatDays ? "Update" : "Create"} reminder
</Button> </Button>
{repeatDays && (
<Button
key="layout-cancel-create-project"
onClick={() => onOpenChange(false, { days: null, note: null })}
colorSchema="danger"
leftIcon={<FontAwesomeIcon icon={faTrash} />}
>
Delete reminder
</Button>
)}
<Button <Button
key="layout-cancel-create-project" key="layout-cancel-create-project"
onClick={() => onOpenChange(false)} onClick={() => onOpenChange(false)}
@@ -5,6 +5,7 @@ import {
faCheckCircle, faCheckCircle,
faCircle, faCircle,
faCircleDot, faCircleDot,
faClock,
faPlus, faPlus,
faTag faTag
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
@@ -33,9 +34,11 @@ import {
Tooltip Tooltip
} from "@app/components/v2"; } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context"; import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
import { useToggle } from "@app/hooks";
import { useGetSecretVersion } from "@app/hooks/api"; import { useGetSecretVersion } from "@app/hooks/api";
import { DecryptedSecret, UserWsKeyPair, WsTag } from "@app/hooks/api/types"; import { DecryptedSecret, UserWsKeyPair, WsTag } from "@app/hooks/api/types";
import { CreateReminderForm } from "./CreateReminderForm";
import { formSchema, SecretActionType, TFormSchema } from "./SecretListView.utils"; import { formSchema, SecretActionType, TFormSchema } from "./SecretListView.utils";
type Props = { type Props = {
@@ -147,262 +150,317 @@ export const SecretDetailSidebar = ({
await onSaveSecret(secret, { ...secret, ...data }, () => reset()); await onSaveSecret(secret, { ...secret, ...data }, () => reset());
}; };
const [createReminderFormOpen, setCreateReminderFormOpen] = useToggle(false);
const secretReminderRepeatDays = watch("reminderRepeatDays");
const secretReminderNote = watch("reminderNote");
return ( return (
<Drawer <>
onOpenChange={(state) => { <CreateReminderForm
if (isOpen && isDirty) { repeatDays={secretReminderRepeatDays}
if ( note={secretReminderNote}
// eslint-disable-next-line no-alert isOpen={createReminderFormOpen}
window.confirm("You have edited the secret. Are you sure you want to reset the change?") onOpenChange={(_, data) => {
) { setCreateReminderFormOpen.toggle();
onToggle(false);
reset(); if (data) {
} else return; setValue("reminderRepeatDays", data.days, { shouldDirty: true });
} setValue("reminderNote", data.note, { shouldDirty: true });
onToggle(state); }
}} }}
isOpen={isOpen} />
> <Drawer
<DrawerContent title="Secret"> onOpenChange={(state) => {
<form onSubmit={handleSubmit(handleFormSubmit)} className="h-full"> if (isOpen && isDirty) {
<div className="flex h-full flex-col"> if (
<FormControl label="Key"> // eslint-disable-next-line no-alert
<Input isDisabled {...register("key")} /> window.confirm(
</FormControl> "You have edited the secret. Are you sure you want to reset the change?"
<ProjectPermissionCan )
I={ProjectPermissionActions.Edit} ) {
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })} onToggle(false);
> reset();
{(isAllowed) => ( } else return;
<Controller }
name="value" onToggle(state);
key="secret-value" }}
control={control} isOpen={isOpen}
render={({ field }) => ( >
<FormControl label="Value"> <DrawerContent title="Secret">
<SecretInput <form onSubmit={handleSubmit(handleFormSubmit)} className="h-full">
isReadOnly={isReadOnly} <div className="flex h-full flex-col">
key="secret-value" <FormControl label="Key">
isDisabled={isOverridden || !isAllowed} <Input isDisabled {...register("key")} />
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-bunker-800 px-2 py-1.5" </FormControl>
{...field}
autoFocus={false}
/>
</FormControl>
)}
/>
)}
</ProjectPermissionCan>
<div className="mb-2 border-b border-mineshaft-600 pb-4">
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Edit} I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })} a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
> >
{(isAllowed) => ( {(isAllowed) => (
<Switch <Controller
isDisabled={!isAllowed} name="value"
id="personal-override" key="secret-value"
onCheckedChange={handleOverrideClick} control={control}
isChecked={isOverridden} render={({ field }) => (
> <FormControl label="Value">
Override with a personal value <SecretInput
</Switch> isReadOnly={isReadOnly}
key="secret-value"
isDisabled={isOverridden || !isAllowed}
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-bunker-800 px-2 py-1.5"
{...field}
autoFocus={false}
/>
</FormControl>
)}
/>
)} )}
</ProjectPermissionCan> </ProjectPermissionCan>
</div> <div className="mb-2 border-b border-mineshaft-600 pb-4">
{isOverridden && (
<Controller
name="valueOverride"
control={control}
render={({ field }) => (
<FormControl label="Value Override">
<SecretInput
isReadOnly={isReadOnly}
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-bunker-800 px-2 py-1.5"
{...field}
/>
</FormControl>
)}
/>
)}
<FormControl label="Tags" className="">
<div className="grid auto-cols-min grid-flow-col gap-2 overflow-hidden pt-2">
{fields.map(({ tagColor, id: formId, name, id }) => (
<Tag
className="flex w-min items-center space-x-2"
key={formId}
onClose={() => {
if (cannotEditSecret) {
createNotification({ type: "error", text: "Access denied" });
return;
}
const tag = tags?.find(({ id: tagId }) => id === tagId);
if (tag) handleTagSelect(tag);
}}
>
<div
className="h-3 w-3 rounded-full"
style={{ backgroundColor: tagColor || "#bec2c8" }}
/>
<div className="text-sm">{name}</div>
</Tag>
))}
<DropdownMenu>
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
>
{(isAllowed) => (
<DropdownMenuTrigger asChild>
<IconButton
ariaLabel="add"
variant="outline_bg"
size="xs"
className="rounded-md"
isDisabled={!isAllowed}
>
<FontAwesomeIcon icon={faPlus} />
</IconButton>
</DropdownMenuTrigger>
)}
</ProjectPermissionCan>
<DropdownMenuContent align="end" className="z-[100]">
<DropdownMenuLabel>Apply tags to this secrets</DropdownMenuLabel>
{tags.map((tag) => {
const { id: tagId, name, color } = tag;
const isSelected = selectedTagsGroupById?.[tagId];
return (
<DropdownMenuItem
onClick={() => handleTagSelect(tag)}
key={tagId}
icon={isSelected && <FontAwesomeIcon icon={faCheckCircle} />}
iconPos="right"
>
<div className="flex items-center">
<div
className="mr-2 h-2 w-2 rounded-full"
style={{ background: color || "#bec2c8" }}
/>
{name}
</div>
</DropdownMenuItem>
);
})}
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={ProjectPermissionSub.Tags}
>
{(isAllowed) => (
<DropdownMenuItem asChild>
<Button
size="xs"
className="w-full"
colorSchema="primary"
variant="outline_bg"
leftIcon={<FontAwesomeIcon icon={faTag} />}
onClick={onCreateTag}
isDisabled={!isAllowed}
>
Create a tag
</Button>
</DropdownMenuItem>
)}
</ProjectPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</div>
</FormControl>
<FormControl label="Comments & Notes">
<TextArea
className="border border-mineshaft-600 text-sm"
{...register("comment")}
readOnly={isReadOnly}
rows={5}
/>
</FormControl>
<div className="my-2 mb-6 border-b border-mineshaft-600 pb-4">
<Controller
control={control}
name="skipMultilineEncoding"
render={({ field: { value, onChange, onBlur } }) => (
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
>
{(isAllowed) => (
<Switch
id="skipmultiencoding-option"
onCheckedChange={(isChecked) => onChange(!isChecked)}
isChecked={!value}
onBlur={onBlur}
isDisabled={!isAllowed}
className="items-center"
>
Enable multi line encoding
<Tooltip
content="Infisical encodes multiline secrets by escaping newlines and wrapping in quotes. To disable, enable this option"
className="z-[100]"
>
<FontAwesomeIcon icon={faCircleQuestion} className="ml-1" size="sm" />
</Tooltip>
</Switch>
)}
</ProjectPermissionCan>
)}
/>
</div>
<div className="dark mb-4 flex-grow text-sm text-bunker-300">
<div className="mb-2">Version History</div>
<div className="flex h-48 flex-col space-y-2 overflow-y-auto overflow-x-hidden rounded-md border border-mineshaft-600 bg-bunker-800 p-2 dark:[color-scheme:dark]">
{secretVersion?.map(({ createdAt, value, id }, i) => (
<div key={id} className="flex flex-col space-y-1">
<div className="flex items-center space-x-2">
<div>
<FontAwesomeIcon icon={i === 0 ? faCircleDot : faCircle} size="sm" />
</div>
<div>{format(new Date(createdAt), "Pp")}</div>
</div>
<div className="ml-1.5 flex items-center space-x-2 border-l border-bunker-300 pl-4">
<div className="self-start rounded-sm bg-primary-500/30 px-1">Value:</div>
<div className="break-all font-mono">{value}</div>
</div>
</div>
))}
</div>
</div>
<div className="flex flex-col space-y-4">
<div className="flex items-center space-x-4">
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Edit} I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })} a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
> >
{(isAllowed) => ( {(isAllowed) => (
<Button <Switch
isFullWidth isDisabled={!isAllowed}
type="submit" id="personal-override"
isDisabled={isSubmitting || !isDirty || !isAllowed} onCheckedChange={handleOverrideClick}
isLoading={isSubmitting} isChecked={isOverridden}
> >
Save Changes Override with a personal value
</Button> </Switch>
)}
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
>
{(isAllowed) => (
<Button colorSchema="danger" isDisabled={!isAllowed} onClick={onDeleteSecret}>
Delete
</Button>
)} )}
</ProjectPermissionCan> </ProjectPermissionCan>
</div> </div>
{isOverridden && (
<Controller
name="valueOverride"
control={control}
render={({ field }) => (
<FormControl label="Value Override">
<SecretInput
isReadOnly={isReadOnly}
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-bunker-800 px-2 py-1.5"
{...field}
/>
</FormControl>
)}
/>
)}
<FormControl label="Tags" className="">
<div className="grid auto-cols-min grid-flow-col gap-2 overflow-hidden pt-2">
{fields.map(({ tagColor, id: formId, name, id }) => (
<Tag
className="flex w-min items-center space-x-2"
key={formId}
onClose={() => {
if (cannotEditSecret) {
createNotification({ type: "error", text: "Access denied" });
return;
}
const tag = tags?.find(({ id: tagId }) => id === tagId);
if (tag) handleTagSelect(tag);
}}
>
<div
className="h-3 w-3 rounded-full"
style={{ backgroundColor: tagColor || "#bec2c8" }}
/>
<div className="text-sm">{name}</div>
</Tag>
))}
<DropdownMenu>
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
>
{(isAllowed) => (
<DropdownMenuTrigger asChild>
<IconButton
ariaLabel="add"
variant="outline_bg"
size="xs"
className="rounded-md"
isDisabled={!isAllowed}
>
<FontAwesomeIcon icon={faPlus} />
</IconButton>
</DropdownMenuTrigger>
)}
</ProjectPermissionCan>
<DropdownMenuContent align="end" className="z-[100]">
<DropdownMenuLabel>Apply tags to this secrets</DropdownMenuLabel>
{tags.map((tag) => {
const { id: tagId, name, color } = tag;
const isSelected = selectedTagsGroupById?.[tagId];
return (
<DropdownMenuItem
onClick={() => handleTagSelect(tag)}
key={tagId}
icon={isSelected && <FontAwesomeIcon icon={faCheckCircle} />}
iconPos="right"
>
<div className="flex items-center">
<div
className="mr-2 h-2 w-2 rounded-full"
style={{ background: color || "#bec2c8" }}
/>
{name}
</div>
</DropdownMenuItem>
);
})}
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={ProjectPermissionSub.Tags}
>
{(isAllowed) => (
<DropdownMenuItem asChild>
<Button
size="xs"
className="w-full"
colorSchema="primary"
variant="outline_bg"
leftIcon={<FontAwesomeIcon icon={faTag} />}
onClick={onCreateTag}
isDisabled={!isAllowed}
>
Create a tag
</Button>
</DropdownMenuItem>
)}
</ProjectPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</div>
</FormControl>
<FormControl label="Reminder">
{secretReminderRepeatDays && secretReminderRepeatDays > 0 ? (
<div className="mt-2 ml-1 flex items-center justify-between">
<div className="flex items-center space-x-2">
<FontAwesomeIcon className="text-primary-500" icon={faClock} />
<span className="text-sm text-bunker-300">
Reminder every {secretReminderRepeatDays}{" "}
{secretReminderRepeatDays > 1 ? "days" : "day"}
</span>
</div>
<div>
<Button
className="px-2 py-1"
variant="outline_bg"
onClick={() => setCreateReminderFormOpen.on()}
>
Update
</Button>
</div>
</div>
) : (
<div className="mt-2 ml-1 flex items-center space-x-2">
<Button
className="px-2 py-1"
variant="outline_bg"
leftIcon={<FontAwesomeIcon icon={faClock} />}
onClick={() => setCreateReminderFormOpen.on()}
>
Create Reminder
</Button>
</div>
)}
</FormControl>
<FormControl label="Comments & Notes">
<TextArea
className="border border-mineshaft-600 text-sm"
{...register("comment")}
readOnly={isReadOnly}
rows={5}
/>
</FormControl>
<div className="my-2 mb-6 border-b border-mineshaft-600 pb-4">
<Controller
control={control}
name="skipMultilineEncoding"
render={({ field: { value, onChange, onBlur } }) => (
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
>
{(isAllowed) => (
<Switch
id="skipmultiencoding-option"
onCheckedChange={(isChecked) => onChange(!isChecked)}
isChecked={!value}
onBlur={onBlur}
isDisabled={!isAllowed}
className="items-center"
>
Enable multi line encoding
<Tooltip
content="Infisical encodes multiline secrets by escaping newlines and wrapping in quotes. To disable, enable this option"
className="z-[100]"
>
<FontAwesomeIcon icon={faCircleQuestion} className="ml-1" size="sm" />
</Tooltip>
</Switch>
)}
</ProjectPermissionCan>
)}
/>
</div>
<div className="dark mb-4 flex-grow text-sm text-bunker-300">
<div className="mb-2">Version History</div>
<div className="flex h-48 flex-col space-y-2 overflow-y-auto overflow-x-hidden rounded-md border border-mineshaft-600 bg-bunker-800 p-2 dark:[color-scheme:dark]">
{secretVersion?.map(({ createdAt, value, id }, i) => (
<div key={id} className="flex flex-col space-y-1">
<div className="flex items-center space-x-2">
<div>
<FontAwesomeIcon icon={i === 0 ? faCircleDot : faCircle} size="sm" />
</div>
<div>{format(new Date(createdAt), "Pp")}</div>
</div>
<div className="ml-1.5 flex items-center space-x-2 border-l border-bunker-300 pl-4">
<div className="self-start rounded-sm bg-primary-500/30 px-1">Value:</div>
<div className="break-all font-mono">{value}</div>
</div>
</div>
))}
</div>
</div>
<div className="flex flex-col space-y-4">
<div className="flex items-center space-x-4">
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
>
{(isAllowed) => (
<Button
isFullWidth
type="submit"
isDisabled={isSubmitting || !isDirty || !isAllowed}
isLoading={isSubmitting}
>
Save Changes
</Button>
)}
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
>
{(isAllowed) => (
<Button colorSchema="danger" isDisabled={!isAllowed} onClick={onDeleteSecret}>
Delete
</Button>
)}
</ProjectPermissionCan>
</div>
</div>
</div> </div>
</div> </form>
</form> </DrawerContent>
</DrawerContent> </Drawer>
</Drawer> </>
); );
}; };
@@ -111,9 +111,11 @@ export const SecretItem = memo(
resolver: zodResolver(formSchema) resolver: zodResolver(formSchema)
}); });
const secretReminderRepeatDays = watch("reminderRepeatDays");
const secretReminderNote = watch("reminderNote");
const overrideAction = watch("overrideAction"); const overrideAction = watch("overrideAction");
const hasComment = Boolean(watch("comment")); const hasComment = Boolean(watch("comment"));
const hasReminder = Boolean(watch("reminderRepeatDays"));
const selectedTags = watch("tags", []); const selectedTags = watch("tags", []);
const selectedTagsGroupById = selectedTags.reduce<Record<string, boolean>>( const selectedTagsGroupById = selectedTags.reduce<Record<string, boolean>>(
@@ -191,6 +193,8 @@ export const SecretItem = memo(
return ( return (
<> <>
<CreateReminderForm <CreateReminderForm
repeatDays={secretReminderRepeatDays}
note={secretReminderNote}
isOpen={createReminderFormOpen} isOpen={createReminderFormOpen}
onOpenChange={(_, data) => { onOpenChange={(_, data) => {
setCreateReminderFormOpen.toggle(); setCreateReminderFormOpen.toggle();
@@ -380,22 +384,24 @@ export const SecretItem = memo(
<IconButton <IconButton
className={twMerge( className={twMerge(
"w-0 overflow-hidden p-0 group-hover:mr-2 group-hover:w-5 data-[state=open]:w-6", "w-0 overflow-hidden p-0 group-hover:mr-2 group-hover:w-5 data-[state=open]:w-6",
hasReminder && "w-5 text-primary" Boolean(secretReminderRepeatDays) && "w-5 text-primary"
)} )}
variant="plain" variant="plain"
size="md" size="md"
ariaLabel="add-reminder" ariaLabel="add-reminder"
> >
<Tooltip content="Reminder"> <Tooltip
content={
secretReminderRepeatDays && secretReminderRepeatDays > 0
? `Every ${secretReminderRepeatDays} day${
Number(secretReminderRepeatDays) > 1 ? "s" : ""
}
`
: "Reminder"
}
>
<FontAwesomeIcon <FontAwesomeIcon
onClick={() => { onClick={() => setCreateReminderFormOpen.on()}
if (!hasReminder) {
setCreateReminderFormOpen.on();
} else {
setValue("reminderRepeatDays", null, { shouldDirty: true });
setValue("reminderNote", null, { shouldDirty: true });
}
}}
icon={faClock} icon={faClock}
/> />
</Tooltip> </Tooltip>
@@ -170,7 +170,11 @@ export const AddAPIKeyModal = ({
> >
Add Add
</Button> </Button>
<Button colorSchema="secondary" variant="plain"> <Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("addAPIKey", false)}
>
Cancel Cancel
</Button> </Button>
</div> </div>
@@ -139,7 +139,7 @@ export const WebhooksTab = withProjectPermission(
}; };
return ( return (
<div className="mb-6 max-w-screen-lg rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"> <div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex justify-between"> <div className="flex justify-between">
<p className="text-xl font-semibold text-mineshaft-100">{t("settings.webhooks.title")}</p> <p className="text-xl font-semibold text-mineshaft-100">{t("settings.webhooks.title")}</p>
<ProjectPermissionCan <ProjectPermissionCan