Compare commits

..

104 Commits

Author SHA1 Message Date
6333eccc4a Create 20240426171026_test.ts 2024-04-26 17:52:02 -04:00
0af2b113df Delete backend/src/db/migrations/20240426171026_test.ts 2024-04-26 17:51:52 -04:00
63a7941047 Update update-be-new-migration-latest-timestamp.yml 2024-04-26 17:51:20 -04:00
edeac08cb5 Merge pull request #1752 from Infisical/maidul98-patch-4
Update 20240426171026_test.ts
2024-04-26 14:54:26 -04:00
019b0ae09a Update 20240426171026_test.ts 2024-04-26 14:54:15 -04:00
1d00bb0a64 Update update-be-new-migration-latest-timestamp.yml 2024-04-26 14:52:47 -04:00
d96f1320ed Merge pull request #1751 from Infisical/revert-1750-revert-1749-revert-1748-revert-1747-revert-1746-revert-1745-revert-1744-revert-1743-revert-1742-revert-1741-revert-1740-revert-1739-test-db-rename
Revert "Revert "Revert "Revert "Revert "Revert "Revert "Revert "Revert "Revert "Revert "Revert "test migration rename""""""""""""
2024-04-26 14:44:10 -04:00
50dbefeb48 Revert "Revert "Revert "Revert "Revert "Revert "Revert "Revert "Revert "Revert "Revert "Revert "test migration rename"""""""""""" 2024-04-26 14:43:57 -04:00
56ac2c6780 Merge pull request #1750 from Infisical/revert-1749-revert-1748-revert-1747-revert-1746-revert-1745-revert-1744-revert-1743-revert-1742-revert-1741-revert-1740-revert-1739-test-db-rename
Revert "Revert "Revert "Revert "Revert "Revert "Revert "Revert "Revert "Revert "Revert "test migration rename"""""""""""
2024-04-26 14:43:54 -04:00
c2f16da411 Revert "Revert "Revert "Revert "Revert "Revert "Revert "Revert "Revert "Revert "Revert "test migration rename""""""""""" 2024-04-26 14:43:46 -04:00
8223aee2ef Update update-be-new-migration-latest-timestamp.yml 2024-04-26 14:43:38 -04:00
5bd2af9621 Merge pull request #1749 from Infisical/revert-1748-revert-1747-revert-1746-revert-1745-revert-1744-revert-1743-revert-1742-revert-1741-revert-1740-revert-1739-test-db-rename
Revert "Revert "Revert "Revert "Revert "Revert "Revert "Revert "Revert "Revert "test migration rename""""""""""
2024-04-26 14:28:44 -04:00
b3df6ce6b5 Revert "Revert "Revert "Revert "Revert "Revert "Revert "Revert "Revert "Revert "test migration rename"""""""""" 2024-04-26 14:28:34 -04:00
e12eb5347d Merge pull request #1748 from Infisical/revert-1747-revert-1746-revert-1745-revert-1744-revert-1743-revert-1742-revert-1741-revert-1740-revert-1739-test-db-rename
Revert "Revert "Revert "Revert "Revert "Revert "Revert "Revert "Revert "test migration rename"""""""""
2024-04-26 14:28:31 -04:00
83a4426d31 Revert "Revert "Revert "Revert "Revert "Revert "Revert "Revert "Revert "test migration rename""""""""" 2024-04-26 14:28:22 -04:00
3fd1fbc355 Update update-be-new-migration-latest-timestamp.yml 2024-04-26 14:28:13 -04:00
306d2b4bd9 Merge pull request #1747 from Infisical/revert-1746-revert-1745-revert-1744-revert-1743-revert-1742-revert-1741-revert-1740-revert-1739-test-db-rename
Revert "Revert "Revert "Revert "Revert "Revert "Revert "Revert "test migration rename""""""""
2024-04-26 14:17:42 -04:00
c2c66af1f9 Revert "Revert "Revert "Revert "Revert "Revert "Revert "Revert "test migration rename"""""""" 2024-04-26 14:17:30 -04:00
7ae65478aa Merge pull request #1746 from Infisical/revert-1745-revert-1744-revert-1743-revert-1742-revert-1741-revert-1740-revert-1739-test-db-rename
Revert "Revert "Revert "Revert "Revert "Revert "Revert "test migration rename"""""""
2024-04-26 14:17:26 -04:00
b1594e65c6 Revert "Revert "Revert "Revert "Revert "Revert "Revert "test migration rename""""""" 2024-04-26 14:17:17 -04:00
0bce5b1daa Update update-be-new-migration-latest-timestamp.yml 2024-04-26 14:16:29 -04:00
207db93483 Merge pull request #1745 from Infisical/revert-1744-revert-1743-revert-1742-revert-1741-revert-1740-revert-1739-test-db-rename
Revert "Revert "Revert "Revert "Revert "Revert "test migration rename""""""
2024-04-26 14:10:22 -04:00
972f6a4887 Revert "Revert "Revert "Revert "Revert "Revert "test migration rename"""""" 2024-04-26 14:09:58 -04:00
6e1bece9d9 Merge pull request #1744 from Infisical/revert-1743-revert-1742-revert-1741-revert-1740-revert-1739-test-db-rename
Revert "Revert "Revert "Revert "Revert "test migration rename"""""
2024-04-26 14:09:41 -04:00
63e8bc1845 Revert "Revert "Revert "Revert "Revert "test migration rename""""" 2024-04-26 14:09:26 -04:00
4f92663b66 Update update-be-new-migration-latest-timestamp.yml 2024-04-26 14:09:15 -04:00
a66a6790c0 Merge pull request #1743 from Infisical/revert-1742-revert-1741-revert-1740-revert-1739-test-db-rename
Revert "Revert "Revert "Revert "test migration rename""""
2024-04-26 14:02:29 -04:00
bde853d280 Update update-be-new-migration-latest-timestamp.yml 2024-04-26 14:01:47 -04:00
acda627236 Revert "Revert "Revert "Revert "test migration rename"""" 2024-04-26 14:01:09 -04:00
875afbb4d6 Merge pull request #1742 from Infisical/revert-1741-revert-1740-revert-1739-test-db-rename
Revert "Revert "Revert "test migration rename"""
2024-04-26 14:01:06 -04:00
56f50a18dc Revert "Revert "Revert "test migration rename""" 2024-04-26 14:00:50 -04:00
801c438d05 Merge pull request #1741 from Infisical/revert-1740-revert-1739-test-db-rename
Revert "Revert "test migration rename""
2024-04-26 13:58:18 -04:00
baba411502 Update update-be-new-migration-latest-timestamp.yml 2024-04-26 13:58:01 -04:00
4c20ac6564 Revert "Revert "test migration rename"" 2024-04-26 13:56:03 -04:00
4e8556dec2 Merge pull request #1740 from Infisical/revert-1739-test-db-rename
Revert "test migration rename"
2024-04-26 13:55:57 -04:00
2d7b9ec1e4 Revert "test migration rename" 2024-04-26 13:55:43 -04:00
8bb9ed4394 Merge pull request #1739 from Infisical/test-db-rename
test migration rename
2024-04-26 13:50:48 -04:00
e4246ae85f Update update-be-new-migration-latest-timestamp.yml 2024-04-26 13:50:22 -04:00
f24067542f test migration rename 2024-04-26 13:10:59 -04:00
a7f5a61f37 Merge pull request #1737 from akhilmhdh/chore/gh-action-migration-rename
feat: github action to rename new migration file to latest timestamp
2024-04-26 13:07:54 -04:00
b5fd7698d8 chore: updated rename migration action to run on PR merge 2024-04-26 22:27:32 +05:30
61c3102573 Merge pull request #1738 from Infisical/sheen/auto-complete-for-path-select
Feature: added auto-complete for secret path inputs
2024-04-26 22:05:21 +05:30
d6a5bf9d50 adjustment: simplified onchange propagation 2024-04-27 00:32:34 +08:00
70f63b3190 Default metadata to empty object if it does not exist on integration for aws integration sync ops 2024-04-26 09:06:01 -07:00
2b0670a409 fix: addressed suggestion issue in copy secrets from board 2024-04-26 19:20:05 +08:00
cc25639157 fix: resolved loop traversal of suggestions 2024-04-26 18:59:39 +08:00
5ff30aed10 fix: addressed suggestion incomplete issue 2024-04-26 18:46:16 +08:00
656ec4bf16 feature: migrated path inputs to new component 2024-04-26 18:18:23 +08:00
0bac9a8e02 feat: github action to rename new migration file to latest timestamp in utc 2024-04-26 15:24:27 +05:30
5142e6e5f6 feature: created secret path input component with autocomplete support 2024-04-26 16:07:27 +08:00
49c735caf9 Merge pull request #1572 from Salman2301/feat-secret-input-autocomplete
Secret input auto complete
2024-04-25 15:48:25 -04:00
b4de2ea85d Merge pull request #1735 from akhilmhdh/import/recursive
feat(server): recursive imported secret fetch for api
2024-04-25 15:44:37 -04:00
8b8baf1ef2 nit: variable rename 2024-04-25 15:40:55 -04:00
2a89b872c5 adjustment: finalized text width 2024-04-26 03:33:58 +08:00
2d2d9a5987 feat(server): added cyclic detector 2024-04-26 01:00:42 +05:30
a20a60850b adjustment: finalized popover positioning 2024-04-26 03:25:34 +08:00
35e38c23dd feat(server): recursive imported secret fetch for api 2024-04-26 00:26:18 +05:30
b79e61c86b Merge remote-tracking branch 'origin/main' into feat-secret-input-autocomplete 2024-04-26 01:40:21 +08:00
e555d3129d fix: resolved invalid handling of undefined vals 2024-04-26 01:22:09 +08:00
a41883137c fix: addressed type check issue 2024-04-26 01:08:31 +08:00
c414bf6c39 Merge pull request #1734 from Infisical/daniel/fix-saml-invite-bug
Fix: SAML organization invite bug
2024-04-25 13:07:01 -04:00
9b782a9da6 adjustment: removed unused component 2024-04-26 00:56:45 +08:00
497c0cf63d adjustment: final ui/ux adjustments 2024-04-26 00:52:37 +08:00
93761f37ea Update saml-config-service.ts 2024-04-25 18:13:42 +02:00
68e530e5d2 Fix: On complete signup, check for saml auth and present org ID and handle membership status 2024-04-25 18:12:08 +02:00
20b1cdf909 adjustment: added click handler to suggestions and finalized env icon 2024-04-25 21:43:25 +08:00
4bae65cc55 adjustment: ux finalization 2024-04-25 12:44:01 +08:00
6da5f12855 Merge pull request #1733 from Infisical/test-ldap-connection
Add Test Connection capability and User Search Filter for LDAP configuration
2024-04-25 00:42:15 -04:00
7a242c4976 Fix frontend type check issue 2024-04-24 21:32:36 -07:00
b01d381993 Refactor ldap filter validation 2024-04-24 21:19:07 -07:00
1ac18fcf0c Merge remote-tracking branch 'origin' into test-ldap-connection 2024-04-24 20:58:40 -07:00
8d5ef5f4d9 Add user search filter field for LDAP and validation for search filters 2024-04-24 20:58:16 -07:00
35b5253853 Update README.md 2024-04-24 19:56:05 -04:00
99d59a38d5 Add test connection btn for LDAP, update group search filter impl, update group search filter examples in docs 2024-04-24 16:50:23 -07:00
9ab1fce0e0 feature: created new secret input component 2024-04-25 04:02:34 +08:00
9992fbf3dd Merge pull request #1729 from Infisical/groups-phase-3
Groups Phase 3 (LDAP)
2024-04-24 08:34:46 -07:00
26778d92d3 adjustment: unified logic for InfisicalSecretInput 2024-04-24 01:40:46 +08:00
b135ba263c adjustment: finalized InfisicalSecretInput 2024-04-23 22:49:00 +08:00
9b7ef55ad7 adjustment: simplified caret helper 2024-04-23 21:43:01 +08:00
872f8bdad8 adjustment: converted remaining salug validation to use slugify 2024-04-23 21:14:33 +08:00
80b0dc6895 adjustment: removed autocomplete from RotationInputForm 2024-04-23 20:55:43 +08:00
20898c00c6 feat: added referencing autocomplete to remaining components 2024-04-23 11:35:17 +08:00
2200bd646e adjustment: added isImport handling 2024-04-23 11:17:12 +08:00
fb69236f47 Merge remote-tracking branch 'origin/main' into feat-secret-input-autocomplete 2024-04-23 11:02:45 +08:00
918734b26b adjustment: used enum for reference type 2024-04-23 10:43:10 +08:00
729c75112b adjustment: deleted unused reference select component 2024-04-23 10:26:41 +08:00
738e8cfc5c adjustment: standardized slug validation 2024-04-23 10:25:36 +08:00
6daeed68a0 adjustment: converted callback functions to use arrow notation 2024-04-23 02:28:42 +08:00
31a499c9cd adjustment: added clearTimeout 2024-04-23 02:10:15 +08:00
358ca3decd adjustment: reverted changes made to SecretInput 2024-04-22 23:26:44 +08:00
0899fdb7d5 adjustment: migrated to InfisicalSecretInput component 2024-04-22 22:22:48 +08:00
f9957e111c feat: move to radix select component 2024-03-16 06:40:15 +05:30
1193e33890 feat: improve validation secret reference 2024-03-16 00:12:50 +05:30
ec64753795 fix: refactor and onyl valid env 2024-03-15 22:49:40 +05:30
c908310f6e fix: add lint line for reference 2024-03-15 21:07:13 +05:30
ee2b8a594a fix: handle skip and slash in environment slug 2024-03-15 20:21:27 +05:30
3ae27e088f feat: move to react query 2024-03-15 17:17:39 +05:30
393c0c9e90 fix: add todo 2024-03-15 03:30:11 +05:30
5e453ab8a6 fix: try dynamic height based on text area height 2024-03-15 03:26:49 +05:30
273c78c0a5 fix: hot fix for onChange 2024-03-15 03:04:36 +05:30
1bcc742466 feat: improve reference match, auto closing tag and reference select 2024-03-15 02:22:09 +05:30
1fc9e60254 feat: fetch folder and secrets 2024-03-14 09:39:57 +05:30
126e385046 fix: layout gap increment on new lines 2024-03-14 06:01:02 +05:30
2f932ad103 feat: add basic ui and icons 2024-03-14 05:55:23 +05:30
52 changed files with 1261 additions and 181 deletions

View File

@ -0,0 +1,26 @@
import os
from datetime import datetime, timedelta
def rename_migrations():
migration_folder = "./backend/src/db/migrations"
with open("added_files.txt", "r") as file:
changed_files = file.readlines()
# Find the latest file among the changed files
latest_timestamp = datetime.now() # utc time
for file_path in changed_files:
file_path = file_path.strip()
# each new file bump by 1s
latest_timestamp = latest_timestamp + timedelta(seconds=1)
new_filename = os.path.join(migration_folder, latest_timestamp.strftime("%Y%m%d%H%M%S") + f"_{file_path.split('_')[1]}")
old_filename = os.path.join(migration_folder, file_path)
os.rename(old_filename, new_filename)
print(f"Renamed {old_filename} to {new_filename}")
if len(changed_files) == 0:
print("No new files added to migration folder")
if __name__ == "__main__":
rename_migrations()

View File

@ -0,0 +1,48 @@
name: Rename Migrations
on:
pull_request:
types: [closed]
paths:
- 'backend/src/db/migrations/**'
jobs:
rename:
runs-on: ubuntu-latest
if: github.event.pull_request.merged == true
steps:
- name: Check out repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Get list of newly added files in migration folder
run: |
git diff --name-status HEAD^ HEAD backend/src/db/migrations | grep '^A' | cut -f2 | xargs -n1 basename > added_files.txt
if [ ! -s added_files.txt ]; then
echo "No new files added. Skipping"
echo "SKIP_RENAME=true" >> $GITHUB_ENV
fi
- name: Script to rename migrations
if: env.SKIP_RENAME != 'true'
run: python .github/resources/rename_migration_files.py
- name: Commit and push changes
if: env.SKIP_RENAME != 'true'
run: |
git config user.name github-actions
git config user.email github-actions@github.com
git add ./backend/src/db/migrations
rm added_files.txt
git commit -m "chore: renamed new migration files to latest timestamp (gh-action)"
- name: Create Pull Request
if: env.SKIP_RENAME != 'true'
uses: peter-evans/create-pull-request@v6
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: 'chore: renamed new migration files to latest UTC (gh-action)'
title: 'GH Action: rename new migration file timestamp'
branch-suffix: timestamp

View File

@ -76,7 +76,7 @@ Check out the [Quickstart Guides](https://infisical.com/docs/getting-started/int
| Use Infisical Cloud | Deploy Infisical on premise |
| ------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| The fastest and most reliable way to <br> get started with Infisical is signing up <br> for free to [Infisical Cloud](https://app.infisical.com/login). | <a href="https://infisical.com/docs/self-hosting/deployment-options/aws-ec2"><img src=".github/images/deploy-to-aws.png" width="150" width="300" /></a> <a href="https://infisical.com/docs/self-hosting/deployment-options/digital-ocean-marketplace" alt="Deploy to DigitalOcean"> <img width="217" alt="Deploy to DO" src="https://www.deploytodo.com/do-btn-blue.svg"/> </a> <br> View all [deployment options](https://infisical.com/docs/self-hosting/overview) |
| The fastest and most reliable way to <br> get started with Infisical is signing up <br> for free to [Infisical Cloud](https://app.infisical.com/login). | <br> View all [deployment options](https://infisical.com/docs/self-hosting/overview) |
### Run Infisical locally

View File

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

View File

@ -0,0 +1 @@

View File

@ -25,7 +25,8 @@ export const LdapConfigsSchema = z.object({
createdAt: z.date(),
updatedAt: z.date(),
groupSearchBase: z.string().default(""),
groupSearchFilter: z.string().default("")
groupSearchFilter: z.string().default(""),
searchFilter: z.string().default("")
});
export type TLdapConfigs = z.infer<typeof LdapConfigsSchema>;

View File

@ -16,7 +16,7 @@ import { z } from "zod";
import { LdapConfigsSchema, LdapGroupMapsSchema } from "@app/db/schemas";
import { TLDAPConfig } from "@app/ee/services/ldap-config/ldap-config-types";
import { searchGroups } from "@app/ee/services/ldap-config/ldap-fns";
import { isValidLdapFilter, searchGroups } from "@app/ee/services/ldap-config/ldap-fns";
import { getConfig } from "@app/lib/config/env";
import { logger } from "@app/lib/logger";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
@ -54,12 +54,19 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => {
try {
const ldapConfig = (req as unknown as FastifyRequest).ldapConfig as TLDAPConfig;
const groupFilter = "(|(memberUid={{.Username}})(member={{.UserDN}})(uniqueMember={{.UserDN}}))";
const searchFilter =
ldapConfig.groupSearchFilter ||
groupFilter.replace("{{.Username}}", user.uid).replace("{{.UserDN}}", user.dn);
let groups: { dn: string; cn: string }[] | undefined;
if (ldapConfig.groupSearchBase) {
const groupFilter = "(|(memberUid={{.Username}})(member={{.UserDN}})(uniqueMember={{.UserDN}}))";
const groupSearchFilter = (ldapConfig.groupSearchFilter || groupFilter)
.replace(/{{\.Username}}/g, user.uid)
.replace(/{{\.UserDN}}/g, user.dn);
const shouldProcessGroups = ldapConfig.groupSearchFilter && ldapConfig.groupSearchBase;
if (!isValidLdapFilter(groupSearchFilter)) {
throw new Error("Generated LDAP search filter is invalid.");
}
groups = await searchGroups(ldapConfig, groupSearchFilter, ldapConfig.groupSearchBase);
}
const { isUserCompleted, providerAuthToken } = await server.services.ldap.ldapLogin({
ldapConfigId: ldapConfig.id,
@ -68,9 +75,7 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => {
firstName: user.givenName ?? user.cn ?? "",
lastName: user.sn ?? "",
emails: user.mail ? [user.mail] : [],
groups: shouldProcessGroups
? await searchGroups(ldapConfig, searchFilter, ldapConfig.groupSearchBase)
: undefined,
groups,
relayState: ((req as unknown as FastifyRequest).body as { RelayState?: string }).RelayState,
orgId: (req as unknown as FastifyRequest).ldapConfig.organization
});
@ -132,6 +137,7 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => {
bindDN: z.string(),
bindPass: z.string(),
searchBase: z.string(),
searchFilter: z.string(),
groupSearchBase: z.string(),
groupSearchFilter: z.string(),
caCert: z.string()
@ -165,8 +171,12 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => {
bindDN: z.string().trim(),
bindPass: z.string().trim(),
searchBase: z.string().trim(),
groupSearchBase: z.string().trim().default(""),
groupSearchFilter: z.string().trim().default(""),
searchFilter: z.string().trim().default("(uid={{username}})"),
groupSearchBase: z.string().trim(),
groupSearchFilter: z
.string()
.trim()
.default("(|(memberUid={{.Username}})(member={{.UserDN}})(uniqueMember={{.UserDN}}))"),
caCert: z.string().trim().default("")
}),
response: {
@ -202,6 +212,7 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => {
bindDN: z.string().trim(),
bindPass: z.string().trim(),
searchBase: z.string().trim(),
searchFilter: z.string().trim(),
groupSearchBase: z.string().trim(),
groupSearchFilter: z.string().trim(),
caCert: z.string().trim()
@ -327,4 +338,32 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => {
return ldapGroupMap;
}
});
server.route({
method: "POST",
url: "/config/:configId/test-connection",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
params: z.object({
configId: z.string().trim()
}),
response: {
200: z.boolean()
}
},
handler: async (req) => {
const result = await server.services.ldap.testLDAPConnection({
actor: req.permission.type,
actorId: req.permission.id,
orgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
ldapConfigId: req.params.configId
});
return result;
}
});
};

View File

@ -37,8 +37,10 @@ import {
TGetLdapCfgDTO,
TGetLdapGroupMapsDTO,
TLdapLoginDTO,
TTestLdapConnectionDTO,
TUpdateLdapCfgDTO
} from "./ldap-config-types";
import { testLDAPConfig } from "./ldap-fns";
import { TLdapGroupMapDALFactory } from "./ldap-group-map-dal";
type TLdapConfigServiceFactoryDep = {
@ -96,6 +98,7 @@ export const ldapConfigServiceFactory = ({
bindDN,
bindPass,
searchBase,
searchFilter,
groupSearchBase,
groupSearchFilter,
caCert
@ -173,6 +176,7 @@ export const ldapConfigServiceFactory = ({
bindPassIV,
bindPassTag,
searchBase,
searchFilter,
groupSearchBase,
groupSearchFilter,
encryptedCACert,
@ -194,6 +198,7 @@ export const ldapConfigServiceFactory = ({
bindDN,
bindPass,
searchBase,
searchFilter,
groupSearchBase,
groupSearchFilter,
caCert
@ -212,6 +217,7 @@ export const ldapConfigServiceFactory = ({
isActive,
url,
searchBase,
searchFilter,
groupSearchBase,
groupSearchFilter
};
@ -315,6 +321,7 @@ export const ldapConfigServiceFactory = ({
bindDN,
bindPass,
searchBase: ldapConfig.searchBase,
searchFilter: ldapConfig.searchFilter,
groupSearchBase: ldapConfig.groupSearchBase,
groupSearchFilter: ldapConfig.groupSearchFilter,
caCert
@ -350,7 +357,7 @@ export const ldapConfigServiceFactory = ({
bindDN: ldapConfig.bindDN,
bindCredentials: ldapConfig.bindPass,
searchBase: ldapConfig.searchBase,
searchFilter: "(uid={{username}})",
searchFilter: ldapConfig.searchFilter || "(uid={{username}})",
// searchAttributes: ["uid", "uidNumber", "givenName", "sn", "mail"],
...(ldapConfig.caCert !== ""
? {
@ -650,6 +657,23 @@ export const ldapConfigServiceFactory = ({
return deletedGroupMap;
};
const testLDAPConnection = async ({ actor, actorId, orgId, actorAuthMethod, actorOrgId }: TTestLdapConnectionDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Ldap);
const plan = await licenseService.getPlan(orgId);
if (!plan.ldap)
throw new BadRequestError({
message: "Failed to test LDAP connection due to plan restriction. Upgrade plan to test the LDAP connection."
});
const ldapConfig = await getLdapCfg({
orgId
});
return testLDAPConfig(ldapConfig);
};
return {
createLdapCfg,
updateLdapCfg,
@ -660,6 +684,7 @@ export const ldapConfigServiceFactory = ({
bootLdap,
getLdapGroupMaps,
createLdapGroupMap,
deleteLdapGroupMap
deleteLdapGroupMap,
testLDAPConnection
};
};

View File

@ -20,6 +20,7 @@ export type TCreateLdapCfgDTO = {
bindDN: string;
bindPass: string;
searchBase: string;
searchFilter: string;
groupSearchBase: string;
groupSearchFilter: string;
caCert: string;
@ -33,6 +34,7 @@ export type TUpdateLdapCfgDTO = {
bindDN: string;
bindPass: string;
searchBase: string;
searchFilter: string;
groupSearchBase: string;
groupSearchFilter: string;
caCert: string;
@ -72,3 +74,7 @@ export type TDeleteLdapGroupMapDTO = {
ldapConfigId: string;
ldapGroupMapId: string;
} & TOrgPermission;
export type TTestLdapConnectionDTO = {
ldapConfigId: string;
} & TOrgPermission;

View File

@ -4,6 +4,65 @@ import { logger } from "@app/lib/logger";
import { TLDAPConfig } from "./ldap-config-types";
export const isValidLdapFilter = (filter: string) => {
try {
ldapjs.parseFilter(filter);
return true;
} catch (error) {
logger.error("Invalid LDAP filter");
logger.error(error);
return false;
}
};
/**
* Test the LDAP configuration by attempting to bind to the LDAP server
* @param ldapConfig - The LDAP configuration to test
* @returns {Boolean} isConnected - Whether or not the connection was successful
*/
export const testLDAPConfig = async (ldapConfig: TLDAPConfig): Promise<boolean> => {
return new Promise((resolve) => {
const ldapClient = ldapjs.createClient({
url: ldapConfig.url,
bindDN: ldapConfig.bindDN,
bindCredentials: ldapConfig.bindPass,
...(ldapConfig.caCert !== ""
? {
tlsOptions: {
ca: [ldapConfig.caCert]
}
}
: {})
});
ldapClient.on("error", (err) => {
logger.error("LDAP client error:", err);
logger.error(err);
resolve(false);
});
ldapClient.bind(ldapConfig.bindDN, ldapConfig.bindPass, (err) => {
if (err) {
logger.error("Error binding to LDAP");
logger.error(err);
ldapClient.unbind();
resolve(false);
} else {
logger.info("Successfully connected and bound to LDAP.");
ldapClient.unbind();
resolve(true);
}
});
});
};
/**
* Search for groups in the LDAP server
* @param ldapConfig - The LDAP configuration to use
* @param filter - The filter to use when searching for groups
* @param base - The base to search from
* @returns
*/
export const searchGroups = async (
ldapConfig: TLDAPConfig,
filter: string,
@ -31,11 +90,7 @@ export const searchGroups = async (
},
(err, res) => {
if (err) {
ldapClient.unbind((unbindError) => {
if (unbindError) {
logger.error("Error unbinding LDAP client:", unbindError);
}
});
ldapClient.unbind();
return reject(err);
}
@ -51,19 +106,11 @@ export const searchGroups = async (
groups.push({ dn, cn });
});
res.on("error", (error) => {
ldapClient.unbind((unbindError) => {
if (unbindError) {
logger.error("Error unbinding LDAP client:", unbindError);
}
});
ldapClient.unbind();
reject(error);
});
res.on("end", () => {
ldapClient.unbind((unbindError) => {
if (unbindError) {
logger.error("Error unbinding LDAP client:", unbindError);
}
});
ldapClient.unbind();
resolve(groups);
});
}

View File

@ -340,11 +340,12 @@ export const samlConfigServiceFactory = ({
orgId,
inviteEmail: email,
role: OrgMembershipRole.Member,
status: OrgMembershipStatus.Accepted
status: user.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later
},
tx
);
} else if (orgMembership.status === OrgMembershipStatus.Invited) {
// Only update the membership to Accepted if the user account is already completed.
} else if (orgMembership.status === OrgMembershipStatus.Invited && user.isAccepted) {
await orgDAL.updateMembershipById(
orgMembership.id,
{

View File

@ -1,3 +1,4 @@
import slugify from "@sindresorhus/slugify";
import { z } from "zod";
import { ProjectEnvironmentsSchema } from "@app/db/schemas";
@ -26,7 +27,13 @@ export const registerProjectEnvRouter = async (server: FastifyZodProvider) => {
}),
body: z.object({
name: z.string().trim().describe(ENVIRONMENTS.CREATE.name),
slug: z.string().trim().describe(ENVIRONMENTS.CREATE.slug)
slug: z
.string()
.trim()
.refine((v) => slugify(v) === v, {
message: "Slug must be a valid slug"
})
.describe(ENVIRONMENTS.CREATE.slug)
}),
response: {
200: z.object({
@ -84,7 +91,14 @@ export const registerProjectEnvRouter = async (server: FastifyZodProvider) => {
id: z.string().trim().describe(ENVIRONMENTS.UPDATE.id)
}),
body: z.object({
slug: z.string().trim().optional().describe(ENVIRONMENTS.UPDATE.slug),
slug: z
.string()
.trim()
.optional()
.refine((v) => !v || slugify(v) === v, {
message: "Slug must be a valid slug"
})
.describe(ENVIRONMENTS.UPDATE.slug),
name: z.string().trim().optional().describe(ENVIRONMENTS.UPDATE.name),
position: z.number().optional().describe(ENVIRONMENTS.UPDATE.position)
}),

View File

@ -4,6 +4,7 @@ import { OrgMembershipStatus } from "@app/db/schemas";
import { convertPendingGroupAdditionsToGroupMemberships } from "@app/ee/services/group/group-fns";
import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors";
import { isDisposableEmail } from "@app/lib/validator";
@ -139,9 +140,11 @@ export const authSignupServiceFactory = ({
throw new Error("Failed to complete account for complete user");
}
let organizationId;
let organizationId: string | null = null;
let authMethod: AuthMethod | null = null;
if (providerAuthToken) {
const { orgId } = validateProviderAuthToken(providerAuthToken, user.username);
const { orgId, authMethod: userAuthMethod } = validateProviderAuthToken(providerAuthToken, user.username);
authMethod = userAuthMethod;
organizationId = orgId;
} else {
validateSignUpAuthorization(authorization, user.id);
@ -165,6 +168,26 @@ export const authSignupServiceFactory = ({
},
tx
);
// If it's SAML Auth and the organization ID is present, we should check if the user has a pending invite for this org, and accept it
if (isAuthMethodSaml(authMethod) && organizationId) {
const [pendingOrgMembership] = await orgDAL.findMembership({
inviteEmail: email,
userId: user.id,
status: OrgMembershipStatus.Invited,
orgId: organizationId
});
if (pendingOrgMembership) {
await orgDAL.updateMembershipById(
pendingOrgMembership.id,
{
status: OrgMembershipStatus.Accepted
},
tx
);
}
}
return { info: us, key: userEncKey };
});

View File

@ -458,7 +458,7 @@ const syncSecretsAWSParameterStore = async ({
});
ssm.config.update(config);
const metadata = z.record(z.any()).parse(integration.metadata);
const metadata = z.record(z.any()).parse(integration.metadata || {});
const params = {
Path: integration.path as string,
@ -544,7 +544,7 @@ const syncSecretsAWSSecretManager = async ({
}) => {
let secretsManager;
const secKeyVal = getSecretKeyValuePair(secrets);
const metadata = z.record(z.any()).parse(integration.metadata);
const metadata = z.record(z.any()).parse(integration.metadata || {});
try {
if (!accessId) return;

View File

@ -1,33 +1,66 @@
import { SecretType, TSecretImports } from "@app/db/schemas";
import { SecretType, TSecretImports, TSecrets } from "@app/db/schemas";
import { groupBy } from "@app/lib/fn";
import { TSecretDALFactory } from "../secret/secret-dal";
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TSecretImportDALFactory } from "./secret-import-dal";
type TSecretImportSecrets = {
secretPath: string;
environment: string;
environmentInfo: {
id: string;
slug: string;
name: string;
};
folderId: string | undefined;
importFolderId: string;
secrets: (TSecrets & { workspace: string; environment: string; _id: string })[];
};
const LEVEL_BREAK = 10;
const getImportUniqKey = (envSlug: string, path: string) => `${envSlug}=${path}`;
export const fnSecretsFromImports = async ({
allowedImports,
allowedImports: possibleCyclicImports,
folderDAL,
secretDAL
secretDAL,
secretImportDAL,
depth = 0,
cyclicDetector = new Set()
}: {
allowedImports: (Omit<TSecretImports, "importEnv"> & {
importEnv: { id: string; slug: string; name: string };
})[];
folderDAL: Pick<TSecretFolderDALFactory, "findByManySecretPath">;
secretDAL: Pick<TSecretDALFactory, "find">;
secretImportDAL: Pick<TSecretImportDALFactory, "findByFolderIds">;
depth?: number;
cyclicDetector?: Set<string>;
}) => {
const importedFolders = await folderDAL.findByManySecretPath(
allowedImports.map(({ importEnv, importPath }) => ({
envId: importEnv.id,
secretPath: importPath
}))
// avoid going more than a depth
if (depth >= LEVEL_BREAK) return [];
const allowedImports = possibleCyclicImports.filter(
({ importPath, importEnv }) => !cyclicDetector.has(getImportUniqKey(importEnv.slug, importPath))
);
const folderIds = importedFolders.map((el) => el?.id).filter(Boolean) as string[];
if (!folderIds.length) {
const importedFolders = (
await folderDAL.findByManySecretPath(
allowedImports.map(({ importEnv, importPath }) => ({
envId: importEnv.id,
secretPath: importPath
}))
)
).filter(Boolean); // remove undefined ones
if (!importedFolders.length) {
return [];
}
const importedFolderIds = importedFolders.map((el) => el?.id) as string[];
const importedFolderGroupBySourceImport = groupBy(importedFolders, (i) => `${i?.envId}-${i?.path}`);
const importedSecrets = await secretDAL.find(
{
$in: { folderId: folderIds },
$in: { folderId: importedFolderIds },
type: SecretType.Shared
},
{
@ -35,18 +68,50 @@ export const fnSecretsFromImports = async ({
}
);
const importedSecsGroupByFolderId = groupBy(importedSecrets, (i) => i.folderId);
return allowedImports.map(({ importPath, importEnv }, i) => ({
secretPath: importPath,
environment: importEnv.slug,
environmentInfo: importEnv,
folderId: 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
secrets: (importedSecsGroupByFolderId?.[importedFolders?.[i]?.id as string] || []).map((item) => ({
...item,
const importedSecretsGroupByFolderId = groupBy(importedSecrets, (i) => i.folderId);
allowedImports.forEach(({ importPath, importEnv }) => {
cyclicDetector.add(getImportUniqKey(importEnv.slug, importPath));
});
// now we need to check recursively deeper imports made inside other imports
// we go level wise meaning we take all imports of a tree level and then go deeper ones level by level
const deeperImports = await secretImportDAL.findByFolderIds(importedFolderIds);
let secretsFromDeeperImports: TSecretImportSecrets[] = [];
if (deeperImports.length) {
secretsFromDeeperImports = await fnSecretsFromImports({
allowedImports: deeperImports,
secretImportDAL,
folderDAL,
secretDAL,
depth: depth + 1,
cyclicDetector
});
}
const secretsFromdeeperImportGroupedByFolderId = groupBy(secretsFromDeeperImports, (i) => i.importFolderId);
const secrets = allowedImports.map(({ importPath, importEnv, id, folderId }, i) => {
const sourceImportFolder = importedFolderGroupBySourceImport[`${importEnv.id}-${importPath}`][0];
const folderDeeperImportSecrets =
secretsFromdeeperImportGroupedByFolderId?.[sourceImportFolder?.id || ""]?.[0]?.secrets || [];
return {
secretPath: importPath,
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.
_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.
}))
}));
environmentInfo: importEnv,
folderId: importedFolders?.[i]?.id,
id,
importFolderId: folderId,
// 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
secrets: (importedSecretsGroupByFolderId?.[importedFolders?.[i]?.id as string] || [])
.map((item) => ({
...item,
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.
_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.
}))
.concat(folderDeeperImportSecrets)
};
});
return secrets;
};

View File

@ -290,7 +290,7 @@ export const secretImportServiceFactory = ({
})
)
);
return fnSecretsFromImports({ allowedImports, folderDAL, secretDAL });
return fnSecretsFromImports({ allowedImports, folderDAL, secretDAL, secretImportDAL });
};
return {

View File

@ -525,7 +525,8 @@ export const secretServiceFactory = ({
const importedSecrets = await fnSecretsFromImports({
allowedImports,
secretDAL,
folderDAL
folderDAL,
secretImportDAL
});
return {
@ -630,7 +631,8 @@ export const secretServiceFactory = ({
const importedSecrets = await fnSecretsFromImports({
allowedImports,
secretDAL,
folderDAL
folderDAL,
secretImportDAL
});
for (let i = importedSecrets.length - 1; i >= 0; i -= 1) {
for (let j = 0; j < importedSecrets[i].secrets.length; j += 1) {

View File

@ -25,9 +25,10 @@ You can configure your organization in Infisical to have members authenticate wi
- URL: The LDAP server to connect to such as `ldap://ldap.your-org.com`, `ldaps://ldap.myorg.com:636` (for connection over SSL/TLS), etc.
- Bind DN: The distinguished name of object to bind when performing the user search such as `cn=infisical,ou=Users,dc=acme,dc=com`.
- Bind Pass: The password to use along with `Bind DN` when performing the user search.
- Search Base / User DN: Base DN under which to perform user search such as `ou=Users,dc=acme,dc=com`
- User Search Base / User DN: Base DN under which to perform user search such as `ou=Users,dc=acme,dc=com`.
- User Search Filter (optional): Template used to construct the LDAP user search filter such as `(uid={{username}})`; use literal `{{username}}` to have the given username used in the search. The default is `(uid={{username}})` which is compatible with several common directory schemas.
- Group Search Base / Group DN (optional): LDAP search base to use for group membership search such as `ou=Groups,dc=acme,dc=com`.
- Group Filter (optional): Template used when constructing the group membership query such as `(objectClass=posixGroup)`. The template can access the following context variables: [`UserDN`, `UserUID`, `UserName`]. The default is `(|(memberUid={{.Username}})(member={{.UserDN}})(uniqueMember={{.UserDN}}))` which is compatible with several common directory schemas.
- Group Filter (optional): Template used when constructing the group membership query such as `(&(objectClass=posixGroup)(memberUid={{.Username}}))`. The template can access the following context variables: [`UserDN`, `UserName`]. The default is `(|(memberUid={{.Username}})(member={{.UserDN}})(uniqueMember={{.UserDN}}))` which is compatible with several common directory schemas.
- CA Certificate: The CA certificate to use when verifying the LDAP server certificate.
<Note>
@ -35,6 +36,14 @@ You can configure your organization in Infisical to have members authenticate wi
</Note>
</Step>
<Step title="Test the LDAP connection">
Once you've filled out the LDAP configuration, you can test that part of the configuration is correct by pressing the **Test Connection** button.
Infisical will attempt to bind to the LDAP server using the provided **URL**, **Bind DN**, and **Bind Pass**. If the operation is successful, then Infisical will display a success message; if not, then Infisical will display an error message and provide a fuller error in the server logs.
![LDAP test connection](/images/platform/ldap/ldap-test-connection.png)
</Step>
<Step title="Define mappings from LDAP groups to groups in Infisical">
In order to sync LDAP groups to Infisical, head to the **LDAP Group Mappings** section to define mappings from LDAP groups to groups in Infisical.

View File

@ -21,7 +21,6 @@ description: "Learn how to configure JumpCloud LDAP for authenticating into Infi
Next, under User Security Settings and Permissions > Permission Settings, check the box next to **Enable as LDAP Bind DN**.
![LDAP JumpCloud](/images/platform/ldap/jumpcloud/ldap-jumpcloud-enable-bind-dn.png)
</Step>
<Step title="Prepare the LDAP configuration in Infisical">
In Infisical, head to your Organization Settings > Security > LDAP and select **Manage**.
@ -35,9 +34,10 @@ description: "Learn how to configure JumpCloud LDAP for authenticating into Infi
- URL: The LDAP server to connect to (`ldaps://ldap.jumpcloud.com:636`).
- Bind DN: The distinguished name of object to bind when performing the user search (`uid=<ldap-user-username>,ou=Users,o=<your-org-id>,dc=jumpcloud,dc=com`).
- Bind Pass: The password to use along with `Bind DN` when performing the user search.
- Search Base / User DN: Base DN under which to perform user search (`ou=Users,o=<your-org-id>,dc=jumpcloud,dc=com`).
- User Search Base / User DN: Base DN under which to perform user search (`ou=Users,o=<your-org-id>,dc=jumpcloud,dc=com`).
- User Search Filter (optional): Template used to construct the LDAP user search filter (`(uid={{username}})`).
- Group Search Base / Group DN (optional): LDAP search base to use for group membership search (`ou=Users,o=<your-org-id>,dc=jumpcloud,dc=com`).
- Group Filter (optional): Template used when constructing the group membership query (`(objectClass=groupOfNames)`).
- Group Filter (optional): Template used when constructing the group membership query (`(&(objectClass=groupOfNames)(member=uid={{.Username}},ou=Users,o=<your-org-id>,dc=jumpcloud,dc=com))`)
- CA Certificate: The CA certificate to use when verifying the LDAP server certificate (instructions to obtain the certificate for JumpCloud [here](https://jumpcloud.com/support/connect-to-ldap-with-tls-ssl)).
<Tip>
@ -47,6 +47,13 @@ description: "Learn how to configure JumpCloud LDAP for authenticating into Infi
in your LDAP instance **ORG DN**.
</Tip>
</Step>
<Step title="Test the LDAP connection">
Once you've filled out the LDAP configuration, you can test that part of the configuration is correct by pressing the **Test Connection** button.
Infisical will attempt to bind to the LDAP server using the provided **URL**, **Bind DN**, and **Bind Pass**. If the operation is successful, then Infisical will display a success message; if not, then Infisical will display an error message and provide a fuller error in the server logs.
![LDAP test connection](/images/platform/ldap/ldap-test-connection.png)
</Step>
<Step title="Define mappings from LDAP groups to groups in Infisical">
In order to sync LDAP groups to Infisical, head to the **LDAP Group Mappings** section to define mappings from LDAP groups to groups in Infisical.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 439 KiB

After

Width:  |  Height:  |  Size: 506 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 501 KiB

View File

@ -38,6 +38,7 @@
"@radix-ui/react-toast": "^1.1.5",
"@radix-ui/react-tooltip": "^1.0.7",
"@reduxjs/toolkit": "^1.8.3",
"@sindresorhus/slugify": "^2.2.1",
"@stripe/react-stripe-js": "^1.16.3",
"@stripe/stripe-js": "^1.46.0",
"@tanstack/react-query": "^4.23.0",
@ -5776,6 +5777,57 @@
"integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==",
"dev": true
},
"node_modules/@sindresorhus/slugify": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/@sindresorhus/slugify/-/slugify-2.2.1.tgz",
"integrity": "sha512-MkngSCRZ8JdSOCHRaYd+D01XhvU3Hjy6MGl06zhOk614hp9EOAp5gIkBeQg7wtmxpitU6eAL4kdiRMcJa2dlrw==",
"dependencies": {
"@sindresorhus/transliterate": "^1.0.0",
"escape-string-regexp": "^5.0.0"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@sindresorhus/slugify/node_modules/escape-string-regexp": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz",
"integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@sindresorhus/transliterate": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/transliterate/-/transliterate-1.6.0.tgz",
"integrity": "sha512-doH1gimEu3A46VX6aVxpHTeHrytJAG6HgdxntYnCFiIFHEM/ZGpG8KiZGBChchjQmG0XFIBL552kBTjVcMZXwQ==",
"dependencies": {
"escape-string-regexp": "^5.0.0"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@sindresorhus/transliterate/node_modules/escape-string-regexp": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz",
"integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@storybook/addon-actions": {
"version": "7.6.8",
"resolved": "https://registry.npmjs.org/@storybook/addon-actions/-/addon-actions-7.6.8.tgz",

View File

@ -46,6 +46,7 @@
"@radix-ui/react-toast": "^1.1.5",
"@radix-ui/react-tooltip": "^1.0.7",
"@reduxjs/toolkit": "^1.8.3",
"@sindresorhus/slugify": "^2.2.1",
"@stripe/react-stripe-js": "^1.16.3",
"@stripe/stripe-js": "^1.46.0",
"@tanstack/react-query": "^4.23.0",

View File

@ -0,0 +1,375 @@
import { TextareaHTMLAttributes, useEffect, useRef, useState } from "react";
import { faCircle, faFolder, faKey } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import * as Popover from "@radix-ui/react-popover";
import { twMerge } from "tailwind-merge";
import { useWorkspace } from "@app/context";
import { useDebounce } from "@app/hooks";
import { useGetFoldersByEnv, useGetProjectSecrets, useGetUserWsKey } from "@app/hooks/api";
import { SecretInput } from "../SecretInput";
const REGEX_UNCLOSED_SECRET_REFERENCE = /\${(?![^{}]*\})/g;
const REGEX_OPEN_SECRET_REFERENCE = /\${/g;
export enum ReferenceType {
ENVIRONMENT = "environment",
FOLDER = "folder",
SECRET = "secret"
}
type Props = TextareaHTMLAttributes<HTMLTextAreaElement> & {
value?: string | null;
isImport?: boolean;
isVisible?: boolean;
isReadOnly?: boolean;
isDisabled?: boolean;
secretPath?: string;
environment?: string;
containerClassName?: string;
};
type ReferenceItem = {
name: string;
type: ReferenceType;
slug?: string;
};
export const InfisicalSecretInput = ({
value: propValue,
isVisible,
containerClassName,
onBlur,
isDisabled,
isImport,
isReadOnly,
secretPath: propSecretPath,
environment: propEnvironment,
onChange,
...props
}: Props) => {
const [inputValue, setInputValue] = useState(propValue ?? "");
const [isSuggestionsOpen, setIsSuggestionsOpen] = useState(false);
const [currentCursorPosition, setCurrentCursorPosition] = useState(0);
const [currentReference, setCurrentReference] = useState<string>("");
const [secretPath, setSecretPath] = useState<string>(propSecretPath || "/");
const [environment, setEnvironment] = useState<string | undefined>(propEnvironment);
const { currentWorkspace } = useWorkspace();
const workspaceId = currentWorkspace?.id || "";
const { data: decryptFileKey } = useGetUserWsKey(workspaceId);
const { data: secrets } = useGetProjectSecrets({
decryptFileKey: decryptFileKey!,
environment: environment || currentWorkspace?.environments?.[0].slug!,
secretPath,
workspaceId
});
const { folderNames: folders } = useGetFoldersByEnv({
path: secretPath,
environments: [environment || currentWorkspace?.environments?.[0].slug!],
projectId: workspaceId
});
const debouncedCurrentReference = useDebounce(currentReference, 100);
const [listReference, setListReference] = useState<ReferenceItem[]>([]);
const [highlightedIndex, setHighlightedIndex] = useState(-1);
const inputRef = useRef<HTMLTextAreaElement>(null);
useEffect(() => {
setInputValue(propValue ?? "");
}, [propValue]);
useEffect(() => {
let currentEnvironment = propEnvironment;
let currentSecretPath = propSecretPath || "/";
if (!currentReference) {
setSecretPath(currentSecretPath);
setEnvironment(currentEnvironment);
return;
}
const isNested = currentReference.includes(".");
if (isNested) {
const [envSlug, ...folderPaths] = currentReference.split(".");
const isValidEnvSlug = currentWorkspace?.environments.find((e) => e.slug === envSlug);
currentEnvironment = isValidEnvSlug ? envSlug : undefined;
// should be based on the last valid section (with .)
folderPaths.pop();
currentSecretPath = `/${folderPaths?.join("/")}`;
}
setSecretPath(currentSecretPath);
setEnvironment(currentEnvironment);
}, [debouncedCurrentReference]);
useEffect(() => {
const currentListReference: ReferenceItem[] = [];
const isNested = currentReference?.includes(".");
if (!currentReference) {
setListReference(currentListReference);
return;
}
if (!environment) {
currentWorkspace?.environments.forEach((env) => {
currentListReference.unshift({
name: env.slug,
type: ReferenceType.ENVIRONMENT
});
});
} else if (isNested) {
folders?.forEach((folder) => {
currentListReference.unshift({ name: folder, type: ReferenceType.FOLDER });
});
} else if (environment) {
currentWorkspace?.environments.forEach((env) => {
currentListReference.unshift({
name: env.slug,
type: ReferenceType.ENVIRONMENT
});
});
}
secrets?.forEach((secret) => {
currentListReference.unshift({ name: secret.key, type: ReferenceType.SECRET });
});
// Get fragment inside currentReference
const searchFragment = isNested ? currentReference.split(".").pop() || "" : currentReference;
const filteredListRef = currentListReference
.filter((suggestionEntry) =>
suggestionEntry.name.toUpperCase().startsWith(searchFragment.toUpperCase())
)
.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()));
setListReference(filteredListRef);
}, [secrets, environment, debouncedCurrentReference]);
const getIndexOfUnclosedRefToTheLeft = (pos: number) => {
// take substring up to pos in order to consider edits for closed references
const unclosedReferenceIndexMatches = [
...inputValue.substring(0, pos).matchAll(REGEX_UNCLOSED_SECRET_REFERENCE)
].map((match) => match.index);
// find unclosed reference index less than the current cursor position
let indexIter = -1;
unclosedReferenceIndexMatches.forEach((index) => {
if (index !== undefined && index > indexIter && index < pos) {
indexIter = index;
}
});
return indexIter;
};
const getIndexOfUnclosedRefToTheRight = (pos: number) => {
const unclosedReferenceIndexMatches = [...inputValue.matchAll(REGEX_OPEN_SECRET_REFERENCE)].map(
(match) => match.index
);
// find the next unclosed reference index to the right of the current cursor position
// this is so that we know the limitation for slicing references
let indexIter = Infinity;
unclosedReferenceIndexMatches.forEach((index) => {
if (index !== undefined && index > pos && index < indexIter) {
indexIter = index;
}
});
return indexIter;
};
const handleKeyUp = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
// open suggestions if current position is to the right of an unclosed secret reference
const indexIter = getIndexOfUnclosedRefToTheLeft(currentCursorPosition);
if (indexIter === -1) {
return;
}
setIsSuggestionsOpen(true);
if (e.key !== "Enter") {
// current reference is then going to be based on the text from the closest ${ to the right
// until the current cursor position
const openReferenceValue = inputValue.slice(indexIter + 2, currentCursorPosition);
setCurrentReference(openReferenceValue);
}
};
const handleSuggestionSelect = (selectedIndex?: number) => {
const selectedSuggestion = listReference[selectedIndex ?? highlightedIndex];
if (!selectedSuggestion) {
return;
}
const leftIndexIter = getIndexOfUnclosedRefToTheLeft(currentCursorPosition);
const rightIndexLimit = getIndexOfUnclosedRefToTheRight(currentCursorPosition);
if (leftIndexIter === -1) {
return;
}
let newValue = "";
const currentOpenRef = inputValue.slice(leftIndexIter + 2, currentCursorPosition);
if (currentOpenRef.includes(".")) {
// append suggestion after last DOT (.)
const lastDotIndex = currentReference.lastIndexOf(".");
const existingPath = currentReference.slice(0, lastDotIndex);
const refEndAfterAppending = Math.min(
leftIndexIter +
3 +
existingPath.length +
selectedSuggestion.name.length +
Number(selectedSuggestion.type !== ReferenceType.SECRET),
rightIndexLimit - 1
);
newValue = `${inputValue.slice(0, leftIndexIter + 2)}${existingPath}.${
selectedSuggestion.name
}${selectedSuggestion.type !== ReferenceType.SECRET ? "." : "}"}${inputValue.slice(
refEndAfterAppending
)}`;
const openReferenceValue = newValue.slice(leftIndexIter + 2, refEndAfterAppending);
setCurrentReference(openReferenceValue);
// add 1 in order to prevent referenceOpen from being triggered by handleKeyUp
setCurrentCursorPosition(refEndAfterAppending + 1);
} else {
// append selectedSuggestion at position after unclosed ${
const refEndAfterAppending = Math.min(
selectedSuggestion.name.length +
leftIndexIter +
2 +
Number(selectedSuggestion.type !== ReferenceType.SECRET),
rightIndexLimit - 1
);
newValue = `${inputValue.slice(0, leftIndexIter + 2)}${selectedSuggestion.name}${
selectedSuggestion.type !== ReferenceType.SECRET ? "." : "}"
}${inputValue.slice(refEndAfterAppending)}`;
const openReferenceValue = newValue.slice(leftIndexIter + 2, refEndAfterAppending);
setCurrentReference(openReferenceValue);
setCurrentCursorPosition(refEndAfterAppending);
}
onChange?.({ target: { value: newValue } } as any);
setInputValue(newValue);
setHighlightedIndex(-1);
setIsSuggestionsOpen(false);
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
const mod = (n: number, m: number) => ((n % m) + m) % m;
if (e.key === "ArrowDown") {
setHighlightedIndex((prevIndex) => mod(prevIndex + 1, listReference.length));
} else if (e.key === "ArrowUp") {
setHighlightedIndex((prevIndex) => mod(prevIndex - 1, listReference.length));
} else if (e.key === "Enter" && highlightedIndex >= 0) {
handleSuggestionSelect();
}
if (["ArrowDown", "ArrowUp", "Enter"].includes(e.key)) {
e.preventDefault();
}
};
const setIsOpen = (isOpen: boolean) => {
setHighlightedIndex(-1);
if (isSuggestionsOpen) {
setIsSuggestionsOpen(isOpen);
}
};
const handleSecretChange = (e: any) => {
// propagate event to react-hook-form onChange
if (onChange) {
onChange(e);
}
setCurrentCursorPosition(inputRef.current?.selectionStart || 0);
setInputValue(e.target.value);
};
return (
<Popover.Root
open={isSuggestionsOpen && listReference.length > 0 && currentReference.length > 0}
onOpenChange={setIsOpen}
>
<Popover.Trigger asChild>
<SecretInput
{...props}
ref={inputRef}
onKeyDown={handleKeyDown}
onKeyUp={handleKeyUp}
value={inputValue}
onChange={handleSecretChange}
containerClassName={containerClassName}
/>
</Popover.Trigger>
<Popover.Content
align="start"
onOpenAutoFocus={(e) => e.preventDefault()}
className={twMerge(
"relative top-2 z-[100] overflow-hidden rounded-md border border-mineshaft-600 bg-mineshaft-900 font-inter text-bunker-100 shadow-md"
)}
style={{
width: "var(--radix-popover-trigger-width)",
maxHeight: "var(--radix-select-content-available-height)"
}}
>
<div className="max-w-60 h-full w-full flex-col items-center justify-center rounded-md text-white">
{listReference.map((item, i) => {
let entryIcon;
if (item.type === ReferenceType.SECRET) {
entryIcon = faKey;
} else if (item.type === ReferenceType.ENVIRONMENT) {
entryIcon = faCircle;
} else {
entryIcon = faFolder;
}
return (
<div
tabIndex={0}
role="button"
onMouseDown={(e) => {
e.preventDefault();
setHighlightedIndex(i);
handleSuggestionSelect(i);
}}
style={{ pointerEvents: "auto" }}
className="flex items-center justify-between border-mineshaft-600 text-left"
key={`secret-reference-secret-${i + 1}`}
>
<div
className={`${
highlightedIndex === i ? "bg-gray-600" : ""
} text-md relative mb-0.5 flex w-full cursor-pointer select-none items-center justify-between rounded-md px-2 py-2 outline-none transition-all hover:bg-mineshaft-500 data-[highlighted]:bg-mineshaft-500`}
>
<div className="flex gap-2">
<div className="flex items-center text-yellow-700">
<FontAwesomeIcon
icon={entryIcon}
size={item.type === ReferenceType.ENVIRONMENT ? "xs" : "1x"}
/>
</div>
<div className="text-md w-10/12 truncate text-left">{item.name}</div>
</div>
</div>
</div>
);
})}
</div>
</Popover.Content>
</Popover.Root>
);
};
InfisicalSecretInput.displayName = "InfisicalSecretInput";

View File

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

View File

@ -0,0 +1,178 @@
import { InputHTMLAttributes, useEffect, useState } from "react";
import { faFolder } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import * as Popover from "@radix-ui/react-popover";
import { twMerge } from "tailwind-merge";
import { useWorkspace } from "@app/context";
import { useDebounce } from "@app/hooks";
import { useGetFoldersByEnv } from "@app/hooks/api";
import { Input } from "../Input";
type Props = Omit<InputHTMLAttributes<HTMLInputElement>, "size" | "onChange"> & {
value?: string | null;
isImport?: boolean;
isVisible?: boolean;
isReadOnly?: boolean;
isDisabled?: boolean;
environment?: string;
containerClassName?: string;
onChange?: (arg: string) => void;
};
export const SecretPathInput = ({
containerClassName,
onChange,
environment,
value: propValue,
...props
}: Props) => {
const [inputValue, setInputValue] = useState(propValue ?? "");
const [secretPath, setSecretPath] = useState("/");
const [suggestions, setSuggestions] = useState<string[]>([]);
const [highlightedIndex, setHighlightedIndex] = useState(-1);
const debouncedInputValue = useDebounce(inputValue, 200);
const { currentWorkspace } = useWorkspace();
const workspaceId = currentWorkspace?.id || "";
const { folderNames: folders } = useGetFoldersByEnv({
path: secretPath,
environments: [environment || currentWorkspace?.environments?.[0].slug!],
projectId: workspaceId
});
useEffect(() => {
setInputValue(propValue ?? "/");
}, [propValue]);
useEffect(() => {
if (environment) {
setInputValue("/");
setSecretPath("/");
onChange?.("/");
}
}, [environment]);
useEffect(() => {
// update secret path if input is valid
if (
(debouncedInputValue.length > 0 &&
debouncedInputValue[debouncedInputValue.length - 1] === "/") ||
debouncedInputValue.length === 0
) {
setSecretPath(debouncedInputValue);
}
// filter suggestions based on matching
const searchFragment = debouncedInputValue.split("/").pop() || "";
const filteredSuggestions = folders
.filter((suggestionEntry) =>
suggestionEntry.toUpperCase().startsWith(searchFragment.toUpperCase())
)
.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
setSuggestions(filteredSuggestions);
}, [debouncedInputValue]);
const handleSuggestionSelect = (selectedIndex: number) => {
if (!suggestions[selectedIndex]) {
return;
}
const validPaths = inputValue.split("/");
validPaths.pop();
const newValue = `${validPaths.join("/")}/${suggestions[selectedIndex]}`;
onChange?.(newValue);
setInputValue(newValue);
setSecretPath(newValue);
setHighlightedIndex(-1);
setSuggestions([]);
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
const mod = (n: number, m: number) => ((n % m) + m) % m;
if (e.key === "ArrowDown") {
setHighlightedIndex((prevIndex) => mod(prevIndex + 1, suggestions.length));
} else if (e.key === "ArrowUp") {
setHighlightedIndex((prevIndex) => mod(prevIndex - 1, suggestions.length));
} else if (e.key === "Enter" && highlightedIndex >= 0) {
handleSuggestionSelect(highlightedIndex);
}
if (["ArrowDown", "ArrowUp", "Enter"].includes(e.key)) {
e.preventDefault();
}
};
const handleInputChange = (e: any) => {
// propagate event to react-hook-form onChange
if (onChange) {
onChange(e.target.value);
}
setInputValue(e.target.value);
};
return (
<Popover.Root
open={suggestions.length > 0 && inputValue.length > 1}
onOpenChange={() => {
setHighlightedIndex(-1);
}}
>
<Popover.Trigger asChild>
<Input
{...props}
type="text"
autoComplete="off"
onKeyDown={handleKeyDown}
value={inputValue}
onChange={handleInputChange}
className={containerClassName}
/>
</Popover.Trigger>
<Popover.Content
align="start"
onOpenAutoFocus={(e) => e.preventDefault()}
className={twMerge(
"relative top-2 z-[100] overflow-hidden rounded-md border border-mineshaft-600 bg-mineshaft-900 font-inter text-bunker-100 shadow-md"
)}
style={{
width: "var(--radix-popover-trigger-width)",
maxHeight: "var(--radix-select-content-available-height)"
}}
>
<div className="max-w-60 h-full w-full flex-col items-center justify-center rounded-md text-white">
{suggestions.map((suggestion, i) => (
<div
tabIndex={0}
role="button"
onMouseDown={(e) => {
e.preventDefault();
setHighlightedIndex(i);
handleSuggestionSelect(i);
}}
style={{ pointerEvents: "auto" }}
className="flex items-center justify-between border-mineshaft-600 text-left"
key={`secret-reference-secret-${i + 1}`}
>
<div
className={`${
highlightedIndex === i ? "bg-gray-600" : ""
} text-md relative mb-0.5 flex w-full cursor-pointer select-none items-center justify-between rounded-md px-2 py-1 outline-none transition-all hover:bg-mineshaft-500 data-[highlighted]:bg-mineshaft-500`}
>
<div className="flex gap-2">
<div className="flex items-center text-yellow-700">
<FontAwesomeIcon icon={faFolder} />
</div>
<div className="text-md w-10/12 truncate text-left">{suggestion}</div>
</div>
</div>
</div>
))}
</div>
</Popover.Content>
</Popover.Root>
);
};

View File

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

View File

@ -2,5 +2,6 @@ export {
useCreateLDAPConfig,
useCreateLDAPGroupMapping,
useDeleteLDAPGroupMapping,
useTestLDAPConnection,
useUpdateLDAPConfig} from "./mutations";
export { useGetLDAPConfig, useGetLDAPGroupMaps } from "./queries";

View File

@ -14,6 +14,7 @@ export const useCreateLDAPConfig = () => {
bindDN,
bindPass,
searchBase,
searchFilter,
groupSearchBase,
groupSearchFilter,
caCert
@ -24,6 +25,7 @@ export const useCreateLDAPConfig = () => {
bindDN: string;
bindPass: string;
searchBase: string;
searchFilter: string;
groupSearchBase: string;
groupSearchFilter: string;
caCert?: string;
@ -35,6 +37,7 @@ export const useCreateLDAPConfig = () => {
bindDN,
bindPass,
searchBase,
searchFilter,
groupSearchBase,
groupSearchFilter,
caCert
@ -58,6 +61,7 @@ export const useUpdateLDAPConfig = () => {
bindDN,
bindPass,
searchBase,
searchFilter,
groupSearchBase,
groupSearchFilter,
caCert
@ -68,6 +72,7 @@ export const useUpdateLDAPConfig = () => {
bindDN?: string;
bindPass?: string;
searchBase?: string;
searchFilter?: string;
groupSearchBase?: string;
groupSearchFilter?: string;
caCert?: string;
@ -79,6 +84,7 @@ export const useUpdateLDAPConfig = () => {
bindDN,
bindPass,
searchBase,
searchFilter,
groupSearchBase,
groupSearchFilter,
caCert
@ -136,3 +142,14 @@ export const useDeleteLDAPGroupMapping = () => {
}
});
};
export const useTestLDAPConnection = () => {
return useMutation({
mutationFn: async (ldapConfigId: string) => {
const { data } = await apiRequest.post<boolean>(
`/api/v1/ldap/config/${ldapConfigId}/test-connection`
);
return data;
}
});
};

View File

@ -4,16 +4,10 @@ import axios from "axios";
import queryString from "query-string";
import { createNotification } from "@app/components/notifications";
import { SecretPathInput } from "@app/components/v2/SecretPathInput";
import { useCreateIntegration, useGetWorkspaceById } from "@app/hooks/api";
import {
Button,
Card,
CardTitle,
FormControl,
Input,
Select,
SelectItem} from "../../../components/v2";
import { Button, Card, CardTitle, FormControl, Select, SelectItem } from "../../../components/v2";
import {
useGetIntegrationAuthApps,
useGetIntegrationAuthById
@ -27,7 +21,6 @@ const cloudflareEnvironments = [
export default function CloudflarePagesIntegrationPage() {
const router = useRouter();
const { mutateAsync } = useCreateIntegration();
const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]);
const [secretPath, setSecretPath] = useState("/");
@ -130,9 +123,10 @@ export default function CloudflarePagesIntegrationPage() {
</Select>
</FormControl>
<FormControl label="Infisical Secret Path" className="mt-2 px-6">
<Input
<SecretPathInput
value={secretPath}
onChange={(evt) => setSecretPath(evt.target.value)}
onChange={(value) => setSecretPath(value)}
environment={selectedSourceEnvironment}
placeholder="Provide a path, default is /"
/>
</FormControl>

View File

@ -4,17 +4,10 @@ import axios from "axios";
import queryString from "query-string";
import { createNotification } from "@app/components/notifications";
import { SecretPathInput } from "@app/components/v2/SecretPathInput";
import { useCreateIntegration, useGetWorkspaceById } from "@app/hooks/api";
import {
Button,
Card,
CardTitle,
FormControl,
Input,
Select,
SelectItem
} from "../../../components/v2";
import { Button, Card, CardTitle, FormControl, Select, SelectItem } from "../../../components/v2";
import {
useGetIntegrationAuthApps,
useGetIntegrationAuthById
@ -23,7 +16,6 @@ import {
export default function CloudflareWorkersIntegrationPage() {
const router = useRouter();
const { mutateAsync } = useCreateIntegration();
const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]);
const { data: workspace } = useGetWorkspaceById(localStorage.getItem("projectData.id") ?? "");
@ -122,9 +114,10 @@ export default function CloudflareWorkersIntegrationPage() {
</Select>
</FormControl>
<FormControl label="Infisical Secret Path" className="mt-2 px-6">
<Input
<SecretPathInput
value={secretPath}
onChange={(evt) => setSecretPath(evt.target.value)}
onChange={(value) => setSecretPath(value)}
environment={selectedSourceEnvironment}
placeholder="Provide a path, default is /"
/>
</FormControl>

View File

@ -11,6 +11,7 @@ import { motion } from "framer-motion";
import queryString from "query-string";
import * as yup from "yup";
import { SecretPathInput } from "@app/components/v2/SecretPathInput";
import { usePopUp } from "@app/hooks";
import { useCreateIntegration } from "@app/hooks/api";
@ -258,7 +259,11 @@ export default function GCPSecretManagerCreateIntegrationPage() {
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="/" />
<SecretPathInput
{...field}
environment={selectedSourceEnvironment}
placeholder="/"
/>
</FormControl>
)}
/>

View File

@ -11,6 +11,7 @@ import { motion } from "framer-motion";
import queryString from "query-string";
import * as yup from "yup";
import { SecretPathInput } from "@app/components/v2/SecretPathInput";
import { usePopUp } from "@app/hooks";
import { useCreateIntegration } from "@app/hooks/api";
@ -268,7 +269,11 @@ export default function GitLabCreateIntegrationPage() {
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="/" />
<SecretPathInput
{...field}
placeholder="/"
environment={selectedSourceEnvironment}
/>
</FormControl>
)}
/>

View File

@ -9,15 +9,8 @@ import { yupResolver } from "@hookform/resolvers/yup";
import queryString from "query-string";
import * as yup from "yup";
import {
Button,
Card,
CardTitle,
FormControl,
Input,
Select,
SelectItem
} from "@app/components/v2";
import { Button, Card, CardTitle, FormControl, Select, SelectItem } from "@app/components/v2";
import { SecretPathInput } from "@app/components/v2/SecretPathInput";
import { useCreateIntegration } from "@app/hooks/api";
import {
useGetIntegrationAuthApps,
@ -38,6 +31,7 @@ export default function HasuraCloudCreateIntegrationPage() {
const {
control,
handleSubmit,
watch,
formState: { isSubmitting }
} = useForm<FormData>({
resolver: yupResolver(schema)
@ -51,6 +45,8 @@ export default function HasuraCloudCreateIntegrationPage() {
(integrationAuthId as string) ?? ""
);
const selectedSourceEnvironment = watch("sourceEnvironment");
const { data: integrationAuthApps, isLoading: isIntegrationAuthAppsLoading } =
useGetIntegrationAuthApps({
integrationAuthId: (integrationAuthId as string) ?? ""
@ -147,7 +143,7 @@ export default function HasuraCloudCreateIntegrationPage() {
name="secretPath"
render={({ field, fieldState: { error } }) => (
<FormControl label="Secrets Path" errorText={error?.message} isError={Boolean(error)}>
<Input {...field} />
<SecretPathInput {...field} environment={selectedSourceEnvironment} />
</FormControl>
)}
/>

View File

@ -17,19 +17,12 @@ import queryString from "query-string";
// import { App, Pipeline } from "@app/hooks/api/integrationAuth/types";
import * as yup from "yup";
import { SecretPathInput } from "@app/components/v2/SecretPathInput";
// import { RadioGroup } from "@app/components/v2/RadioGroup";
import { useCreateIntegration } from "@app/hooks/api";
import { IntegrationSyncBehavior } from "@app/hooks/api/integrations/types";
import {
Button,
Card,
CardTitle,
FormControl,
Input,
Select,
SelectItem
} from "../../../components/v2";
import { Button, Card, CardTitle, FormControl, Select, SelectItem } from "../../../components/v2";
import {
useGetIntegrationAuthApps,
useGetIntegrationAuthById
@ -280,7 +273,11 @@ export default function HerokuCreateIntegrationPage() {
name="secretPath"
render={({ field, fieldState: { error } }) => (
<FormControl label="Secrets Path" isError={Boolean(error)} errorText={error?.message}>
<Input {...field} placeholder="/" />
<SecretPathInput
{...field}
placeholder="/"
environment={selectedSourceEnvironment}
/>
</FormControl>
)}
/>

View File

@ -15,6 +15,7 @@ import { yupResolver } from "@hookform/resolvers/yup";
import queryString from "query-string";
import * as yup from "yup";
import { SecretPathInput } from "@app/components/v2/SecretPathInput";
import { useCreateIntegration } from "@app/hooks/api";
import {
@ -22,7 +23,6 @@ import {
Card,
CardTitle,
FormControl,
Input,
Select,
SelectItem,
Switch
@ -185,7 +185,11 @@ export default function RenderCreateIntegrationPage() {
name="secretPath"
render={({ field, fieldState: { error } }) => (
<FormControl label="Secrets Path" isError={Boolean(error)} errorText={error?.message}>
<Input {...field} placeholder="/" />
<SecretPathInput
{...field}
placeholder="/"
environment={selectedSourceEnvironment}
/>
</FormControl>
)}
/>

View File

@ -34,6 +34,7 @@ import {
Tag,
Tooltip
} from "@app/components/v2";
import { SecretPathInput } from "@app/components/v2/SecretPathInput";
import {
ProjectPermissionActions,
ProjectPermissionSub,
@ -115,6 +116,7 @@ const SpecificPrivilegeSecretForm = ({
});
const temporaryAccessField = privilegeForm.watch("temporaryAccess");
const selectedEnvironmentSlug = privilegeForm.watch("environmentSlug");
const isTemporary = temporaryAccessField?.isTemporary;
const isExpired =
temporaryAccessField.isTemporary &&
@ -220,7 +222,12 @@ const SpecificPrivilegeSecretForm = ({
name="secretPath"
render={({ field }) => (
<FormControl label="Secret Path">
<Input {...field} isDisabled={isMemberEditDisabled} className="w-48" />
<SecretPathInput
{...field}
isDisabled={isMemberEditDisabled}
environment={selectedEnvironmentSlug}
containerClassName="w-48"
/>
</FormControl>
)}
/>

View File

@ -34,6 +34,7 @@ import {
Tag,
Tooltip
} from "@app/components/v2";
import { SecretPathInput } from "@app/components/v2/SecretPathInput";
import {
ProjectPermissionActions,
ProjectPermissionSub,
@ -107,6 +108,7 @@ const SpecificPrivilegeSecretForm = ({ privilege }: { privilege: TProjectUserPri
});
const temporaryAccessField = privilegeForm.watch("temporaryAccess");
const selectedEnvironmentSlug = privilegeForm.watch("environmentSlug");
const isTemporary = temporaryAccessField?.isTemporary;
const isExpired =
temporaryAccessField.isTemporary &&
@ -208,7 +210,12 @@ const SpecificPrivilegeSecretForm = ({ privilege }: { privilege: TProjectUserPri
name="secretPath"
render={({ field }) => (
<FormControl label="Secret Path">
<Input {...field} isDisabled={isMemberEditDisabled} className="w-48" />
<SecretPathInput
{...field}
isDisabled={isMemberEditDisabled}
containerClassName="w-48"
environment={selectedEnvironmentSlug}
/>
</FormControl>
)}
/>

View File

@ -20,6 +20,7 @@ import {
Select,
SelectItem
} from "@app/components/v2";
import { SecretPathInput } from "@app/components/v2/SecretPathInput";
import { useWorkspace } from "@app/context";
import { useCreateSecretApprovalPolicy, useUpdateSecretApprovalPolicy } from "@app/hooks/api";
import { TSecretApprovalPolicy } from "@app/hooks/api/types";
@ -59,13 +60,14 @@ export const SecretPolicyForm = ({
control,
handleSubmit,
reset,
watch,
formState: { isSubmitting }
} = useForm<TFormSchema>({
resolver: zodResolver(formSchema),
values: editValues ? { ...editValues, environment: editValues.environment.slug } : undefined
});
const { currentWorkspace } = useWorkspace();
const selectedEnvironment = watch("environment");
const environments = currentWorkspace?.environments || [];
useEffect(() => {
@ -174,7 +176,11 @@ export const SecretPolicyForm = ({
name="secretPath"
render={({ field, fieldState: { error } }) => (
<FormControl label="Secret Path" isError={Boolean(error)} errorText={error?.message}>
<Input {...field} value={field.value || ""} />
<SecretPathInput
{...field}
value={field.value || ""}
environment={selectedEnvironment}
/>
</FormControl>
)}
/>

View File

@ -4,15 +4,8 @@ import { AxiosError } from "axios";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import {
Button,
FormControl,
Input,
Modal,
ModalContent,
Select,
SelectItem
} from "@app/components/v2";
import { Button, FormControl, Modal, ModalContent, Select, SelectItem } from "@app/components/v2";
import { SecretPathInput } from "@app/components/v2/SecretPathInput";
import { useWorkspace } from "@app/context";
import { useCreateSecretImport } from "@app/hooks/api";
@ -50,12 +43,12 @@ export const CreateSecretImportForm = ({
handleSubmit,
control,
reset,
watch,
formState: { isSubmitting }
} = useForm<TFormSchema>({ resolver: zodResolver(typeSchema) });
const { currentWorkspace } = useWorkspace();
const environments = currentWorkspace?.environments || [];
const selectedEnvironment = watch("environment");
const { mutateAsync: createSecretImport } = useCreateSecretImport();
@ -130,7 +123,7 @@ export const CreateSecretImportForm = ({
defaultValue="/"
render={({ field, fieldState: { error } }) => (
<FormControl label="Secret Path" isError={Boolean(error)} errorText={error?.message}>
<Input {...field} />
<SecretPathInput {...field} environment={selectedEnvironment} />
</FormControl>
)}
/>

View File

@ -3,7 +3,8 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Input, Modal, ModalContent, SecretInput } from "@app/components/v2";
import { Button, FormControl, Input, Modal, ModalContent } from "@app/components/v2";
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
import { useCreateSecretV3 } from "@app/hooks/api";
import { UserWsKeyPair } from "@app/hooks/api/types";
@ -44,8 +45,6 @@ export const CreateSecretForm = ({
const { isOpen } = usePopUpState(PopUpNames.CreateSecretForm);
const { closePopUp, togglePopUp } = usePopUpAction();
const { mutateAsync: createSecretV3 } = useCreateSecretV3();
const handleFormSubmit = async ({ key, value }: TFormSchema) => {
@ -103,8 +102,10 @@ export const CreateSecretForm = ({
isError={Boolean(errors?.value)}
errorText={errors?.value?.message}
>
<SecretInput
<InfisicalSecretInput
{...field}
environment={environment}
secretPath={secretPath}
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-mineshaft-900 px-2 py-1.5"
/>
</FormControl>

View File

@ -28,6 +28,7 @@ import {
Skeleton,
Tooltip
} from "@app/components/v2";
import { SecretPathInput } from "@app/components/v2/SecretPathInput";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { useDebounce } from "@app/hooks";
import { useGetProjectSecrets } from "@app/hooks/api";
@ -76,7 +77,6 @@ export const CopySecretsFromBoard = ({
handleSubmit,
control,
watch,
register,
reset,
setValue,
formState: { isDirty }
@ -192,9 +192,19 @@ export const CopySecretsFromBoard = ({
</FormControl>
)}
/>
<FormControl label="Secret Path" className="flex-grow" isRequired>
<Input {...register("secretPath")} placeholder="Provide a path, default is /" />
</FormControl>
<Controller
control={control}
name="secretPath"
render={({ field }) => (
<FormControl label="Secret Path" className="flex-grow" isRequired>
<SecretPathInput
{...field}
placeholder="Provide a path, default is /"
environment={selectedEnvSlug}
/>
</FormControl>
)}
/>
</div>
<div className="border-t border-mineshaft-600 pt-4">
<div className="mb-4 flex items-center justify-between">

View File

@ -27,12 +27,12 @@ import {
FormControl,
IconButton,
Input,
SecretInput,
Switch,
Tag,
TextArea,
Tooltip
} from "@app/components/v2";
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
import { useToggle } from "@app/hooks";
import { useGetSecretVersion } from "@app/hooks/api";
@ -71,7 +71,6 @@ export const SecretDetailSidebar = ({
environment,
secretPath
}: Props) => {
const {
register,
control,
@ -204,8 +203,10 @@ export const SecretDetailSidebar = ({
control={control}
render={({ field }) => (
<FormControl label="Value">
<SecretInput
<InfisicalSecretInput
isReadOnly={isReadOnly}
environment={environment}
secretPath={secretPath}
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"
@ -240,8 +241,10 @@ export const SecretDetailSidebar = ({
control={control}
render={({ field }) => (
<FormControl label="Value Override">
<SecretInput
<InfisicalSecretInput
isReadOnly={isReadOnly}
environment={environment}
secretPath={secretPath}
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-bunker-800 px-2 py-1.5"
{...field}
/>

View File

@ -14,7 +14,6 @@ import {
Popover,
PopoverContent,
PopoverTrigger,
SecretInput,
Spinner,
TextArea,
Tooltip
@ -49,6 +48,7 @@ import { memo, useEffect } from "react";
import { Controller, useFieldArray, useForm } from "react-hook-form";
import { twMerge } from "tailwind-merge";
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
import { CreateReminderForm } from "./CreateReminderForm";
import { formSchema, SecretActionType, TFormSchema } from "./SecretListView.utils";
@ -263,10 +263,12 @@ export const SecretItem = memo(
key="value-overriden"
control={control}
render={({ field }) => (
<SecretInput
<InfisicalSecretInput
key="value-overriden"
isVisible={isVisible}
isReadOnly={isReadOnly}
environment={environment}
secretPath={secretPath}
{...field}
containerClassName="py-1.5 rounded-md transition-all group-hover:mr-2"
/>
@ -278,10 +280,12 @@ export const SecretItem = memo(
key="secret-value"
control={control}
render={({ field }) => (
<SecretInput
<InfisicalSecretInput
isReadOnly={isReadOnly}
key="secret-value"
isVisible={isVisible}
environment={environment}
secretPath={secretPath}
{...field}
containerClassName="py-1.5 rounded-md transition-all group-hover:mr-2"
/>

View File

@ -120,7 +120,9 @@ export const SecretItem = ({ mode, preSecret, postSecret }: Props) => {
<Td className="border-r border-mineshaft-600">Value</Td>
{isModified && (
<Td className="border-r border-mineshaft-600">
<SecretInput value={preSecret?.value} />
<SecretInput
value={preSecret?.value}
/>
</Td>
)}
<Td>

View File

@ -13,9 +13,9 @@ import {
Input,
Modal,
ModalContent,
SecretInput,
Tooltip
} from "@app/components/v2";
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
import { useWorkspace } from "@app/context";
import { useCreateFolder, useCreateSecretV3, useUpdateSecretV3 } from "@app/hooks/api";
import { DecryptedSecret, UserWsKeyPair } from "@app/hooks/api/types";
@ -64,8 +64,6 @@ export const CreateSecretForm = ({
const workspaceId = currentWorkspace?.id || "";
const environments = currentWorkspace?.environments || [];
const { mutateAsync: createSecretV3 } = useCreateSecretV3();
const { mutateAsync: updateSecretV3 } = useUpdateSecretV3();
const { mutateAsync: createFolder } = useCreateFolder();
@ -163,7 +161,7 @@ export const CreateSecretForm = ({
isError={Boolean(errors?.value)}
errorText={errors?.value?.message}
>
<SecretInput
<InfisicalSecretInput
{...field}
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-mineshaft-900 px-2 py-1.5"
/>

View File

@ -6,7 +6,8 @@ import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import { IconButton, SecretInput, Tooltip } from "@app/components/v2";
import { IconButton, Tooltip } from "@app/components/v2";
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { useToggle } from "@app/hooks";
@ -49,7 +50,6 @@ export const SecretEditRow = ({
}
});
const [isDeleting, setIsDeleting] = useToggle();
const handleFormReset = () => {
reset();
@ -97,10 +97,13 @@ export const SecretEditRow = ({
control={control}
name="value"
render={({ field }) => (
<SecretInput
<InfisicalSecretInput
{...field}
value={field.value as string}
key="secret-input"
isVisible={isVisible}
secretPath={secretPath}
environment={environment}
isImport={isImportedSecret}
/>
)}

View File

@ -3,6 +3,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Button, FormControl, Input, Select, SelectItem, Spinner } from "@app/components/v2";
import { SecretPathInput } from "@app/components/v2/SecretPathInput";
import { useWorkspace } from "@app/context";
import { useGetProjectSecrets, useGetUserWsKey } from "@app/hooks/api";
@ -78,7 +79,7 @@ export const RotationOutputForm = ({ onSubmit, onCancel, outputSchema = {} }: Pr
defaultValue="/"
render={({ field }) => (
<FormControl className="capitalize" label="Secret path">
<Input {...field} />
<SecretPathInput {...field} environment={environment} />
</FormControl>
)}
/>

View File

@ -6,7 +6,12 @@ import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Input, Modal, ModalContent, TextArea } from "@app/components/v2";
import { useOrganization } from "@app/context";
import { useCreateLDAPConfig, useGetLDAPConfig, useUpdateLDAPConfig } from "@app/hooks/api";
import {
useCreateLDAPConfig,
useGetLDAPConfig,
useTestLDAPConnection,
useUpdateLDAPConfig
} from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
const LDAPFormSchema = z.object({
@ -14,6 +19,7 @@ const LDAPFormSchema = z.object({
bindDN: z.string().default(""),
bindPass: z.string().default(""),
searchBase: z.string().default(""),
searchFilter: z.string().default(""),
groupSearchBase: z.string().default(""),
groupSearchFilter: z.string().default(""),
caCert: z.string().optional()
@ -32,12 +38,22 @@ export const LDAPModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props)
const { mutateAsync: createMutateAsync, isLoading: createIsLoading } = useCreateLDAPConfig();
const { mutateAsync: updateMutateAsync, isLoading: updateIsLoading } = useUpdateLDAPConfig();
const { mutateAsync: testLDAPConnection } = useTestLDAPConnection();
const { data } = useGetLDAPConfig(currentOrg?.id ?? "");
const { control, handleSubmit, reset } = useForm<TLDAPFormData>({
const { control, handleSubmit, reset, watch } = useForm<TLDAPFormData>({
resolver: zodResolver(LDAPFormSchema)
});
const watchUrl = watch("url");
const watchBindDN = watch("bindDN");
const watchBindPass = watch("bindPass");
const watchSearchBase = watch("searchBase");
const watchSearchFilter = watch("searchFilter");
const watchGroupSearchBase = watch("groupSearchBase");
const watchGroupSearchFilter = watch("groupSearchFilter");
const watchCaCert = watch("caCert");
useEffect(() => {
if (data) {
reset({
@ -45,6 +61,7 @@ export const LDAPModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props)
bindDN: data?.bindDN ?? "",
bindPass: data?.bindPass ?? "",
searchBase: data?.searchBase ?? "",
searchFilter: data?.searchFilter ?? "",
groupSearchBase: data?.groupSearchBase ?? "",
groupSearchFilter: data?.groupSearchFilter ?? "",
caCert: data?.caCert ?? ""
@ -57,10 +74,12 @@ export const LDAPModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props)
bindDN,
bindPass,
searchBase,
searchFilter,
groupSearchBase,
groupSearchFilter,
caCert
}: TLDAPFormData) => {
caCert,
shouldCloseModal = true
}: TLDAPFormData & { shouldCloseModal?: boolean }) => {
try {
if (!currentOrg) return;
@ -72,6 +91,7 @@ export const LDAPModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props)
bindDN,
bindPass,
searchBase,
searchFilter,
groupSearchBase,
groupSearchFilter,
caCert
@ -84,13 +104,16 @@ export const LDAPModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props)
bindDN,
bindPass,
searchBase,
searchFilter,
groupSearchBase,
groupSearchFilter,
caCert
});
}
handlePopUpClose("addLDAP");
if (shouldCloseModal) {
handlePopUpClose("addLDAP");
}
createNotification({
text: `Successfully ${!data ? "added" : "updated"} LDAP configuration`,
@ -105,6 +128,45 @@ export const LDAPModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props)
}
};
const handleTestLDAPConnection = async () => {
try {
await onSSOModalSubmit({
url: watchUrl,
bindDN: watchBindDN,
bindPass: watchBindPass,
searchBase: watchSearchBase,
searchFilter: watchSearchFilter,
groupSearchBase: watchGroupSearchBase,
groupSearchFilter: watchGroupSearchFilter,
caCert: watchCaCert,
shouldCloseModal: false
});
if (!data) return;
const result = await testLDAPConnection(data.id);
if (!result) {
createNotification({
text: "Failed to test the LDAP connection: Bind operation was unsuccessful",
type: "error"
});
return;
}
createNotification({
text: "Successfully tested the LDAP connection: Bind operation was successful",
type: "success"
});
} catch (err) {
console.error(err);
createNotification({
text: "Failed to test the LDAP connection",
type: "error"
});
}
};
return (
<Modal
isOpen={popUp?.addLDAP?.isOpen}
@ -147,7 +209,7 @@ export const LDAPModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props)
name="searchBase"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Search Base / User DN"
label="User Search Base / User DN"
errorText={error?.message}
isError={Boolean(error)}
>
@ -155,6 +217,19 @@ export const LDAPModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props)
</FormControl>
)}
/>
<Controller
control={control}
name="searchFilter"
render={({ field, fieldState: { error } }) => (
<FormControl
label="User Search Filter (Optional)"
errorText={error?.message}
isError={Boolean(error)}
>
<Input {...field} placeholder="(uid={{username}})" />
</FormControl>
)}
/>
<Controller
control={control}
name="groupSearchBase"
@ -177,7 +252,10 @@ export const LDAPModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props)
errorText={error?.message}
isError={Boolean(error)}
>
<Input {...field} placeholder="(objectClass=posixGroup)" />
<Input
{...field}
placeholder="(&(objectClass=posixGroup)(memberUid={{.Username}}))"
/>
</FormControl>
)}
/>
@ -203,12 +281,8 @@ export const LDAPModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props)
>
{!data ? "Add" : "Update"}
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpClose("addLDAP")}
>
Cancel
<Button colorSchema="secondary" onClick={handleTestLDAPConnection}>
Test Connection
</Button>
</div>
</form>

View File

@ -68,6 +68,7 @@ export const OrgLDAPSection = (): JSX.Element => {
bindDN: "",
bindPass: "",
searchBase: "",
searchFilter: "",
groupSearchBase: "",
groupSearchFilter: ""
});
@ -97,13 +98,15 @@ export const OrgLDAPSection = (): JSX.Element => {
<div className="py-4">
<div className="mb-2 flex items-center justify-between">
<h2 className="text-md text-mineshaft-100">LDAP</h2>
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Ldap}>
{(isAllowed) => (
<Button onClick={addLDAPBtnClick} colorSchema="secondary" isDisabled={!isAllowed}>
Manage
</Button>
)}
</OrgPermissionCan>
<div className="flex">
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Ldap}>
{(isAllowed) => (
<Button onClick={addLDAPBtnClick} colorSchema="secondary" isDisabled={!isAllowed}>
Manage
</Button>
)}
</OrgPermissionCan>
</div>
</div>
<p className="text-sm text-mineshaft-300">Manage LDAP authentication configuration</p>
</div>

View File

@ -1,5 +1,6 @@
import { Controller, useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import slugify from "@sindresorhus/slugify";
import * as yup from "yup";
import { createNotification } from "@app/components/notifications";
@ -16,7 +17,14 @@ type Props = {
const schema = yup.object({
environmentName: yup.string().label("Environment Name").required(),
environmentSlug: yup.string().label("Environment Slug").required()
environmentSlug: yup
.string()
.label("Environment Slug")
.test({
test: (slug) => slugify(slug as string) === slug,
message: "Slug must be a valid slug"
})
.required()
});
export type FormData = yup.InferType<typeof schema>;

View File

@ -1,5 +1,6 @@
import { Controller, useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import slugify from "@sindresorhus/slugify";
import * as yup from "yup";
import { createNotification } from "@app/components/notifications";
@ -16,13 +17,19 @@ type Props = {
const schema = yup.object({
name: yup.string().label("Environment Name").required(),
slug: yup.string().label("Environment Slug").required()
slug: yup
.string()
.label("Environment Slug")
.test({
test: (slug) => slugify(slug as string) === slug,
message: "Slug must be a valid slug"
})
.required()
});
export type FormData = yup.InferType<typeof schema>;
export const UpdateEnvironmentModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props) => {
const { currentWorkspace } = useWorkspace();
const { mutateAsync, isLoading } = useUpdateWsEnvironment();
const { control, handleSubmit, reset } = useForm<FormData>({
@ -108,7 +115,11 @@ export const UpdateEnvironmentModal = ({ popUp, handlePopUpClose, handlePopUpTog
Update
</Button>
<Button onClick={() => handlePopUpClose("updateEnv")} colorSchema="secondary" variant="plain">
<Button
onClick={() => handlePopUpClose("updateEnv")}
colorSchema="secondary"
variant="plain"
>
Cancel
</Button>
</div>