Compare commits
2 Commits
maidul98-p
...
maidul-122
Author | SHA1 | Date | |
---|---|---|---|
8f3883c7d4 | |||
38cfb7fd41 |
@ -8,10 +8,16 @@ ENCRYPTION_KEY=6c1fe4e407b8911c104518103505b218
|
||||
# THIS IS A SAMPLE AUTH_SECRET KEY AND SHOULD NEVER BE USED FOR PRODUCTION
|
||||
AUTH_SECRET=5lrMXKKWCVocS/uerPsl7V+TX/aaUaI7iDkgl3tSmLE=
|
||||
|
||||
DB_CONNECTION_URI=postgres://infisical:infisical@haproxy:5433/infisical?sslmode=no-verify
|
||||
# Redis
|
||||
REDIS_URL=redis://:123456@haproxy:6379
|
||||
# Postgres creds
|
||||
POSTGRES_PASSWORD=infisical
|
||||
POSTGRES_USER=infisical
|
||||
POSTGRES_DB=infisical
|
||||
|
||||
# Required
|
||||
DB_CONNECTION_URI=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
|
||||
|
||||
# Redis
|
||||
REDIS_URL=redis://redis:6379
|
||||
|
||||
# Website URL
|
||||
# Required
|
26
.github/resources/rename_migration_files.py
vendored
@ -1,26 +0,0 @@
|
||||
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()
|
||||
|
@ -1,8 +1,6 @@
|
||||
name: Build and release CLI
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
push:
|
||||
# run only against tags
|
||||
tags:
|
||||
@ -16,12 +14,6 @@ jobs:
|
||||
cli-integration-tests:
|
||||
name: Run tests before deployment
|
||||
uses: ./.github/workflows/run-cli-tests.yml
|
||||
secrets:
|
||||
CLI_TESTS_UA_CLIENT_ID: ${{ secrets.CLI_TESTS_UA_CLIENT_ID }}
|
||||
CLI_TESTS_UA_CLIENT_SECRET: ${{ secrets.CLI_TESTS_UA_CLIENT_SECRET }}
|
||||
CLI_TESTS_SERVICE_TOKEN: ${{ secrets.CLI_TESTS_SERVICE_TOKEN }}
|
||||
CLI_TESTS_PROJECT_ID: ${{ secrets.CLI_TESTS_PROJECT_ID }}
|
||||
CLI_TESTS_ENV_SLUG: ${{ secrets.CLI_TESTS_ENV_SLUG }}
|
||||
|
||||
goreleaser:
|
||||
runs-on: ubuntu-20.04
|
||||
|
13
.github/workflows/run-cli-tests.yml
vendored
@ -6,20 +6,7 @@ on:
|
||||
paths:
|
||||
- "cli/**"
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
workflow_call:
|
||||
secrets:
|
||||
CLI_TESTS_UA_CLIENT_ID:
|
||||
required: true
|
||||
CLI_TESTS_UA_CLIENT_SECRET:
|
||||
required: true
|
||||
CLI_TESTS_SERVICE_TOKEN:
|
||||
required: true
|
||||
CLI_TESTS_PROJECT_ID:
|
||||
required: true
|
||||
CLI_TESTS_ENV_SLUG:
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
|
@ -1,48 +0,0 @@
|
||||
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
|
@ -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). | <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). | <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) |
|
||||
|
||||
### Run Infisical locally
|
||||
|
||||
|
117
backend/package-lock.json
generated
@ -45,7 +45,6 @@
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"jsrp": "^0.2.4",
|
||||
"knex": "^3.0.1",
|
||||
"ldapjs": "^3.0.7",
|
||||
"libsodium-wrappers": "^0.7.13",
|
||||
"lodash.isequal": "^4.5.0",
|
||||
"ms": "^2.1.3",
|
||||
@ -2511,83 +2510,6 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@ldapjs/asn1": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@ldapjs/asn1/-/asn1-2.0.0.tgz",
|
||||
"integrity": "sha512-G9+DkEOirNgdPmD0I8nu57ygQJKOOgFEMKknEuQvIHbGLwP3ny1mY+OTUYLCbCaGJP4sox5eYgBJRuSUpnAddA=="
|
||||
},
|
||||
"node_modules/@ldapjs/attribute": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@ldapjs/attribute/-/attribute-1.0.0.tgz",
|
||||
"integrity": "sha512-ptMl2d/5xJ0q+RgmnqOi3Zgwk/TMJYG7dYMC0Keko+yZU6n+oFM59MjQOUht5pxJeS4FWrImhu/LebX24vJNRQ==",
|
||||
"dependencies": {
|
||||
"@ldapjs/asn1": "2.0.0",
|
||||
"@ldapjs/protocol": "^1.2.1",
|
||||
"process-warning": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ldapjs/change": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@ldapjs/change/-/change-1.0.0.tgz",
|
||||
"integrity": "sha512-EOQNFH1RIku3M1s0OAJOzGfAohuFYXFY4s73wOhRm4KFGhmQQ7MChOh2YtYu9Kwgvuq1B0xKciXVzHCGkB5V+Q==",
|
||||
"dependencies": {
|
||||
"@ldapjs/asn1": "2.0.0",
|
||||
"@ldapjs/attribute": "1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ldapjs/controls": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@ldapjs/controls/-/controls-2.1.0.tgz",
|
||||
"integrity": "sha512-2pFdD1yRC9V9hXfAWvCCO2RRWK9OdIEcJIos/9cCVP9O4k72BY1bLDQQ4KpUoJnl4y/JoD4iFgM+YWT3IfITWw==",
|
||||
"dependencies": {
|
||||
"@ldapjs/asn1": "^1.2.0",
|
||||
"@ldapjs/protocol": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@ldapjs/controls/node_modules/@ldapjs/asn1": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@ldapjs/asn1/-/asn1-1.2.0.tgz",
|
||||
"integrity": "sha512-KX/qQJ2xxzvO2/WOvr1UdQ+8P5dVvuOLk/C9b1bIkXxZss8BaR28njXdPgFCpj5aHaf1t8PmuVnea+N9YG9YMw=="
|
||||
},
|
||||
"node_modules/@ldapjs/dn": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@ldapjs/dn/-/dn-1.1.0.tgz",
|
||||
"integrity": "sha512-R72zH5ZeBj/Fujf/yBu78YzpJjJXG46YHFo5E4W1EqfNpo1UsVPqdLrRMXeKIsJT3x9dJVIfR6OpzgINlKpi0A==",
|
||||
"dependencies": {
|
||||
"@ldapjs/asn1": "2.0.0",
|
||||
"process-warning": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ldapjs/filter": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@ldapjs/filter/-/filter-2.1.1.tgz",
|
||||
"integrity": "sha512-TwPK5eEgNdUO1ABPBUQabcZ+h9heDORE4V9WNZqCtYLKc06+6+UAJ3IAbr0L0bYTnkkWC/JEQD2F+zAFsuikNw==",
|
||||
"dependencies": {
|
||||
"@ldapjs/asn1": "2.0.0",
|
||||
"@ldapjs/protocol": "^1.2.1",
|
||||
"process-warning": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ldapjs/messages": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@ldapjs/messages/-/messages-1.3.0.tgz",
|
||||
"integrity": "sha512-K7xZpXJ21bj92jS35wtRbdcNrwmxAtPwy4myeh9duy/eR3xQKvikVycbdWVzkYEAVE5Ce520VXNOwCHjomjCZw==",
|
||||
"dependencies": {
|
||||
"@ldapjs/asn1": "^2.0.0",
|
||||
"@ldapjs/attribute": "^1.0.0",
|
||||
"@ldapjs/change": "^1.0.0",
|
||||
"@ldapjs/controls": "^2.1.0",
|
||||
"@ldapjs/dn": "^1.1.0",
|
||||
"@ldapjs/filter": "^2.1.1",
|
||||
"@ldapjs/protocol": "^1.2.1",
|
||||
"process-warning": "^2.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ldapjs/protocol": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@ldapjs/protocol/-/protocol-1.2.1.tgz",
|
||||
"integrity": "sha512-O89xFDLW2gBoZWNXuXpBSM32/KealKCTb3JGtJdtUQc7RjAk8XzrRgyz02cPAwGKwKPxy0ivuC7UP9bmN87egQ=="
|
||||
},
|
||||
"node_modules/@lukeed/ms": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@lukeed/ms/-/ms-2.0.1.tgz",
|
||||
@ -9382,7 +9304,15 @@
|
||||
"node": ">=0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ldapauth-fork/node_modules/ldapjs": {
|
||||
"node_modules/ldapauth-fork/node_modules/lru-cache": {
|
||||
"version": "7.18.3",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
|
||||
"integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/ldapjs": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/ldapjs/-/ldapjs-2.3.3.tgz",
|
||||
"integrity": "sha512-75QiiLJV/PQqtpH+HGls44dXweviFwQ6SiIK27EqzKQ5jU/7UFrl2E5nLdQ3IYRBzJ/AVFJI66u0MZ0uofKYwg==",
|
||||
@ -9400,35 +9330,6 @@
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ldapauth-fork/node_modules/lru-cache": {
|
||||
"version": "7.18.3",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
|
||||
"integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/ldapjs": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/ldapjs/-/ldapjs-3.0.7.tgz",
|
||||
"integrity": "sha512-1ky+WrN+4CFMuoekUOv7Y1037XWdjKpu0xAPwSP+9KdvmV9PG+qOKlssDV6a+U32apwxdD3is/BZcWOYzN30cg==",
|
||||
"dependencies": {
|
||||
"@ldapjs/asn1": "^2.0.0",
|
||||
"@ldapjs/attribute": "^1.0.0",
|
||||
"@ldapjs/change": "^1.0.0",
|
||||
"@ldapjs/controls": "^2.1.0",
|
||||
"@ldapjs/dn": "^1.1.0",
|
||||
"@ldapjs/filter": "^2.1.1",
|
||||
"@ldapjs/messages": "^1.3.0",
|
||||
"@ldapjs/protocol": "^1.2.1",
|
||||
"abstract-logging": "^2.0.1",
|
||||
"assert-plus": "^1.0.0",
|
||||
"backoff": "^2.5.0",
|
||||
"once": "^1.4.0",
|
||||
"vasync": "^2.2.1",
|
||||
"verror": "^1.10.1"
|
||||
}
|
||||
},
|
||||
"node_modules/leven": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz",
|
||||
|
@ -106,7 +106,6 @@
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"jsrp": "^0.2.4",
|
||||
"knex": "^3.0.1",
|
||||
"ldapjs": "^3.0.7",
|
||||
"libsodium-wrappers": "^0.7.13",
|
||||
"lodash.isequal": "^4.5.0",
|
||||
"ms": "^2.1.3",
|
||||
|
4
backend/src/@types/knex.d.ts
vendored
@ -74,9 +74,6 @@ import {
|
||||
TLdapConfigs,
|
||||
TLdapConfigsInsert,
|
||||
TLdapConfigsUpdate,
|
||||
TLdapGroupMaps,
|
||||
TLdapGroupMapsInsert,
|
||||
TLdapGroupMapsUpdate,
|
||||
TOrganizations,
|
||||
TOrganizationsInsert,
|
||||
TOrganizationsUpdate,
|
||||
@ -401,7 +398,6 @@ declare module "knex/types/tables" {
|
||||
>;
|
||||
[TableName.SamlConfig]: Knex.CompositeTableType<TSamlConfigs, TSamlConfigsInsert, TSamlConfigsUpdate>;
|
||||
[TableName.LdapConfig]: Knex.CompositeTableType<TLdapConfigs, TLdapConfigsInsert, TLdapConfigsUpdate>;
|
||||
[TableName.LdapGroupMap]: Knex.CompositeTableType<TLdapGroupMaps, TLdapGroupMapsInsert, TLdapGroupMapsUpdate>;
|
||||
[TableName.OrgBot]: Knex.CompositeTableType<TOrgBots, TOrgBotsInsert, TOrgBotsUpdate>;
|
||||
[TableName.AuditLog]: Knex.CompositeTableType<TAuditLogs, TAuditLogsInsert, TAuditLogsUpdate>;
|
||||
[TableName.GitAppInstallSession]: Knex.CompositeTableType<
|
||||
|
@ -1,34 +0,0 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (!(await knex.schema.hasTable(TableName.LdapGroupMap))) {
|
||||
await knex.schema.createTable(TableName.LdapGroupMap, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.uuid("ldapConfigId").notNullable();
|
||||
t.foreign("ldapConfigId").references("id").inTable(TableName.LdapConfig).onDelete("CASCADE");
|
||||
t.string("ldapGroupCN").notNullable();
|
||||
t.uuid("groupId").notNullable();
|
||||
t.foreign("groupId").references("id").inTable(TableName.Groups).onDelete("CASCADE");
|
||||
t.unique(["ldapGroupCN", "groupId", "ldapConfigId"]);
|
||||
});
|
||||
}
|
||||
|
||||
await createOnUpdateTrigger(knex, TableName.LdapGroupMap);
|
||||
|
||||
await knex.schema.alterTable(TableName.LdapConfig, (t) => {
|
||||
t.string("groupSearchBase").notNullable().defaultTo("");
|
||||
t.string("groupSearchFilter").notNullable().defaultTo("");
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.dropTableIfExists(TableName.LdapGroupMap);
|
||||
await dropOnUpdateTrigger(knex, TableName.LdapGroupMap);
|
||||
await knex.schema.alterTable(TableName.LdapConfig, (t) => {
|
||||
t.dropColumn("groupSearchBase");
|
||||
t.dropColumn("groupSearchFilter");
|
||||
});
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
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");
|
||||
});
|
||||
}
|
@ -22,7 +22,6 @@ export * from "./incident-contacts";
|
||||
export * from "./integration-auths";
|
||||
export * from "./integrations";
|
||||
export * from "./ldap-configs";
|
||||
export * from "./ldap-group-maps";
|
||||
export * from "./models";
|
||||
export * from "./org-bots";
|
||||
export * from "./org-memberships";
|
||||
|
@ -23,10 +23,7 @@ export const LdapConfigsSchema = z.object({
|
||||
caCertIV: z.string(),
|
||||
caCertTag: z.string(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
groupSearchBase: z.string().default(""),
|
||||
groupSearchFilter: z.string().default(""),
|
||||
searchFilter: z.string().default("")
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TLdapConfigs = z.infer<typeof LdapConfigsSchema>;
|
||||
|
@ -1,19 +0,0 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const LdapGroupMapsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
ldapConfigId: z.string().uuid(),
|
||||
ldapGroupCN: z.string(),
|
||||
groupId: z.string().uuid()
|
||||
});
|
||||
|
||||
export type TLdapGroupMaps = z.infer<typeof LdapGroupMapsSchema>;
|
||||
export type TLdapGroupMapsInsert = Omit<z.input<typeof LdapGroupMapsSchema>, TImmutableDBKeys>;
|
||||
export type TLdapGroupMapsUpdate = Partial<Omit<z.input<typeof LdapGroupMapsSchema>, TImmutableDBKeys>>;
|
@ -60,7 +60,6 @@ export enum TableName {
|
||||
SecretRotationOutput = "secret_rotation_outputs",
|
||||
SamlConfig = "saml_configs",
|
||||
LdapConfig = "ldap_configs",
|
||||
LdapGroupMap = "ldap_group_maps",
|
||||
AuditLog = "audit_logs",
|
||||
GitAppInstallSession = "git_app_install_sessions",
|
||||
GitAppOrg = "git_app_org",
|
||||
|
@ -14,9 +14,7 @@ import { FastifyRequest } from "fastify";
|
||||
import LdapStrategy from "passport-ldapauth";
|
||||
import { z } from "zod";
|
||||
|
||||
import { LdapConfigsSchema, LdapGroupMapsSchema } from "@app/db/schemas";
|
||||
import { TLDAPConfig } from "@app/ee/services/ldap-config/ldap-config-types";
|
||||
import { isValidLdapFilter, searchGroups } from "@app/ee/services/ldap-config/ldap-fns";
|
||||
import { LdapConfigsSchema } from "@app/db/schemas";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
@ -52,38 +50,20 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => {
|
||||
// eslint-disable-next-line
|
||||
async (req: IncomingMessage, user, cb) => {
|
||||
try {
|
||||
const ldapConfig = (req as unknown as FastifyRequest).ldapConfig as TLDAPConfig;
|
||||
|
||||
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);
|
||||
|
||||
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,
|
||||
externalId: user.uidNumber,
|
||||
username: user.uid,
|
||||
firstName: user.givenName ?? user.cn ?? "",
|
||||
lastName: user.sn ?? "",
|
||||
firstName: user.givenName,
|
||||
lastName: user.sn,
|
||||
emails: user.mail ? [user.mail] : [],
|
||||
groups,
|
||||
relayState: ((req as unknown as FastifyRequest).body as { RelayState?: string }).RelayState,
|
||||
orgId: (req as unknown as FastifyRequest).ldapConfig.organization
|
||||
});
|
||||
|
||||
return cb(null, { isUserCompleted, providerAuthToken });
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return cb(error, false);
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
return cb(err, false);
|
||||
}
|
||||
}
|
||||
)
|
||||
@ -137,9 +117,6 @@ 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()
|
||||
})
|
||||
}
|
||||
@ -171,12 +148,6 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => {
|
||||
bindDN: z.string().trim(),
|
||||
bindPass: z.string().trim(),
|
||||
searchBase: z.string().trim(),
|
||||
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: {
|
||||
@ -212,9 +183,6 @@ 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()
|
||||
})
|
||||
.partial()
|
||||
@ -236,134 +204,4 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => {
|
||||
return ldap;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/config/:configId/group-maps",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
schema: {
|
||||
params: z.object({
|
||||
configId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
ldapConfigId: z.string(),
|
||||
ldapGroupCN: z.string(),
|
||||
group: z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
slug: z.string()
|
||||
})
|
||||
})
|
||||
)
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const ldapGroupMaps = await server.services.ldap.getLdapGroupMaps({
|
||||
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 ldapGroupMaps;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/config/:configId/group-maps",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
schema: {
|
||||
params: z.object({
|
||||
configId: z.string().trim()
|
||||
}),
|
||||
body: z.object({
|
||||
ldapGroupCN: z.string().trim(),
|
||||
groupSlug: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: LdapGroupMapsSchema
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const ldapGroupMap = await server.services.ldap.createLdapGroupMap({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
orgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
ldapConfigId: req.params.configId,
|
||||
...req.body
|
||||
});
|
||||
return ldapGroupMap;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "DELETE",
|
||||
url: "/config/:configId/group-maps/:groupMapId",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
schema: {
|
||||
params: z.object({
|
||||
configId: z.string().trim(),
|
||||
groupMapId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: LdapGroupMapsSchema
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const ldapGroupMap = await server.services.ldap.deleteLdapGroupMap({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
orgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
ldapConfigId: req.params.configId,
|
||||
ldapGroupMapId: req.params.groupMapId
|
||||
});
|
||||
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;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -22,6 +22,10 @@ const addAcceptedUsersToGroup = async ({
|
||||
projectBotDAL,
|
||||
tx
|
||||
}: TAddUsersToGroup) => {
|
||||
console.log("addAcceptedUsersToGroup args: ", {
|
||||
userIds,
|
||||
group
|
||||
});
|
||||
const users = await userDAL.findUserEncKeyByUserIdsBatch(
|
||||
{
|
||||
userIds
|
||||
|
@ -2,9 +2,6 @@ import { ForbiddenError } from "@casl/ability";
|
||||
import jwt from "jsonwebtoken";
|
||||
|
||||
import { OrgMembershipRole, OrgMembershipStatus, SecretKeyEncoding, TLdapConfigsUpdate } from "@app/db/schemas";
|
||||
import { TGroupDALFactory } from "@app/ee/services/group/group-dal";
|
||||
import { addUsersToGroupByUserIds, removeUsersFromGroupByUserIds } from "@app/ee/services/group/group-fns";
|
||||
import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import {
|
||||
decryptSymmetric,
|
||||
@ -16,12 +13,8 @@ import {
|
||||
} from "@app/lib/crypto/encryption";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type";
|
||||
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
|
||||
import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal";
|
||||
import { TOrgDALFactory } from "@app/services/org/org-dal";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
|
||||
import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal";
|
||||
import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||
import { normalizeUsername } from "@app/services/user/user-fns";
|
||||
import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal";
|
||||
@ -30,40 +23,16 @@ import { TLicenseServiceFactory } from "../license/license-service";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||
import { TLdapConfigDALFactory } from "./ldap-config-dal";
|
||||
import {
|
||||
TCreateLdapCfgDTO,
|
||||
TCreateLdapGroupMapDTO,
|
||||
TDeleteLdapGroupMapDTO,
|
||||
TGetLdapCfgDTO,
|
||||
TGetLdapGroupMapsDTO,
|
||||
TLdapLoginDTO,
|
||||
TTestLdapConnectionDTO,
|
||||
TUpdateLdapCfgDTO
|
||||
} from "./ldap-config-types";
|
||||
import { testLDAPConfig } from "./ldap-fns";
|
||||
import { TLdapGroupMapDALFactory } from "./ldap-group-map-dal";
|
||||
import { TCreateLdapCfgDTO, TGetLdapCfgDTO, TLdapLoginDTO, TUpdateLdapCfgDTO } from "./ldap-config-types";
|
||||
|
||||
type TLdapConfigServiceFactoryDep = {
|
||||
ldapConfigDAL: Pick<TLdapConfigDALFactory, "create" | "update" | "findOne">;
|
||||
ldapGroupMapDAL: Pick<TLdapGroupMapDALFactory, "find" | "create" | "delete" | "findLdapGroupMapsByLdapConfigId">;
|
||||
ldapConfigDAL: TLdapConfigDALFactory;
|
||||
orgDAL: Pick<
|
||||
TOrgDALFactory,
|
||||
"createMembership" | "updateMembershipById" | "findMembership" | "findOrgById" | "findOne" | "updateById"
|
||||
>;
|
||||
orgBotDAL: Pick<TOrgBotDALFactory, "findOne" | "create" | "transaction">;
|
||||
groupDAL: Pick<TGroupDALFactory, "find" | "findOne">;
|
||||
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
|
||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany" | "delete">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser">;
|
||||
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
|
||||
userGroupMembershipDAL: Pick<
|
||||
TUserGroupMembershipDALFactory,
|
||||
"find" | "transaction" | "insertMany" | "filterProjectsByUserMembership" | "delete"
|
||||
>;
|
||||
userDAL: Pick<
|
||||
TUserDALFactory,
|
||||
"create" | "findOne" | "transaction" | "updateById" | "findUserEncKeyByUserIdsBatch" | "find"
|
||||
>;
|
||||
userDAL: Pick<TUserDALFactory, "create" | "findOne" | "transaction" | "updateById">;
|
||||
userAliasDAL: Pick<TUserAliasDALFactory, "create" | "findOne">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
@ -73,15 +42,8 @@ export type TLdapConfigServiceFactory = ReturnType<typeof ldapConfigServiceFacto
|
||||
|
||||
export const ldapConfigServiceFactory = ({
|
||||
ldapConfigDAL,
|
||||
ldapGroupMapDAL,
|
||||
orgDAL,
|
||||
orgBotDAL,
|
||||
groupDAL,
|
||||
groupProjectDAL,
|
||||
projectKeyDAL,
|
||||
projectDAL,
|
||||
projectBotDAL,
|
||||
userGroupMembershipDAL,
|
||||
userDAL,
|
||||
userAliasDAL,
|
||||
permissionService,
|
||||
@ -98,9 +60,6 @@ export const ldapConfigServiceFactory = ({
|
||||
bindDN,
|
||||
bindPass,
|
||||
searchBase,
|
||||
searchFilter,
|
||||
groupSearchBase,
|
||||
groupSearchFilter,
|
||||
caCert
|
||||
}: TCreateLdapCfgDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
@ -176,9 +135,6 @@ export const ldapConfigServiceFactory = ({
|
||||
bindPassIV,
|
||||
bindPassTag,
|
||||
searchBase,
|
||||
searchFilter,
|
||||
groupSearchBase,
|
||||
groupSearchFilter,
|
||||
encryptedCACert,
|
||||
caCertIV,
|
||||
caCertTag
|
||||
@ -198,9 +154,6 @@ export const ldapConfigServiceFactory = ({
|
||||
bindDN,
|
||||
bindPass,
|
||||
searchBase,
|
||||
searchFilter,
|
||||
groupSearchBase,
|
||||
groupSearchFilter,
|
||||
caCert
|
||||
}: TUpdateLdapCfgDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
@ -216,10 +169,7 @@ export const ldapConfigServiceFactory = ({
|
||||
const updateQuery: TLdapConfigsUpdate = {
|
||||
isActive,
|
||||
url,
|
||||
searchBase,
|
||||
searchFilter,
|
||||
groupSearchBase,
|
||||
groupSearchFilter
|
||||
searchBase
|
||||
};
|
||||
|
||||
const orgBot = await orgBotDAL.findOne({ orgId });
|
||||
@ -321,9 +271,6 @@ export const ldapConfigServiceFactory = ({
|
||||
bindDN,
|
||||
bindPass,
|
||||
searchBase: ldapConfig.searchBase,
|
||||
searchFilter: ldapConfig.searchFilter,
|
||||
groupSearchBase: ldapConfig.groupSearchBase,
|
||||
groupSearchFilter: ldapConfig.groupSearchFilter,
|
||||
caCert
|
||||
};
|
||||
};
|
||||
@ -357,8 +304,8 @@ export const ldapConfigServiceFactory = ({
|
||||
bindDN: ldapConfig.bindDN,
|
||||
bindCredentials: ldapConfig.bindPass,
|
||||
searchBase: ldapConfig.searchBase,
|
||||
searchFilter: ldapConfig.searchFilter || "(uid={{username}})",
|
||||
// searchAttributes: ["uid", "uidNumber", "givenName", "sn", "mail"],
|
||||
searchFilter: "(uid={{username}})",
|
||||
searchAttributes: ["uid", "uidNumber", "givenName", "sn", "mail"],
|
||||
...(ldapConfig.caCert !== ""
|
||||
? {
|
||||
tlsOptions: {
|
||||
@ -373,17 +320,7 @@ export const ldapConfigServiceFactory = ({
|
||||
return { opts, ldapConfig };
|
||||
};
|
||||
|
||||
const ldapLogin = async ({
|
||||
ldapConfigId,
|
||||
externalId,
|
||||
username,
|
||||
firstName,
|
||||
lastName,
|
||||
emails,
|
||||
groups,
|
||||
orgId,
|
||||
relayState
|
||||
}: TLdapLoginDTO) => {
|
||||
const ldapLogin = async ({ externalId, username, firstName, lastName, emails, orgId, relayState }: TLdapLoginDTO) => {
|
||||
const appCfg = getConfig();
|
||||
let userAlias = await userAliasDAL.findOne({
|
||||
externalId,
|
||||
@ -457,84 +394,7 @@ export const ldapConfigServiceFactory = ({
|
||||
});
|
||||
}
|
||||
|
||||
const user = await userDAL.transaction(async (tx) => {
|
||||
const newUser = await userDAL.findOne({ id: userAlias.userId }, tx);
|
||||
if (groups) {
|
||||
const ldapGroupIdsToBePartOf = (
|
||||
await ldapGroupMapDAL.find({
|
||||
ldapConfigId,
|
||||
$in: {
|
||||
ldapGroupCN: groups.map((group) => group.cn)
|
||||
}
|
||||
})
|
||||
).map((groupMap) => groupMap.groupId);
|
||||
|
||||
const groupsToBePartOf = await groupDAL.find({
|
||||
orgId,
|
||||
$in: {
|
||||
id: ldapGroupIdsToBePartOf
|
||||
}
|
||||
});
|
||||
const toBePartOfGroupIdsSet = new Set(groupsToBePartOf.map((groupToBePartOf) => groupToBePartOf.id));
|
||||
|
||||
const allLdapGroupMaps = await ldapGroupMapDAL.find({
|
||||
ldapConfigId
|
||||
});
|
||||
|
||||
const ldapGroupIdsCurrentlyPartOf = (
|
||||
await userGroupMembershipDAL.find({
|
||||
userId: newUser.id,
|
||||
$in: {
|
||||
groupId: allLdapGroupMaps.map((groupMap) => groupMap.groupId)
|
||||
}
|
||||
})
|
||||
).map((userGroupMembership) => userGroupMembership.groupId);
|
||||
|
||||
const userGroupMembershipGroupIdsSet = new Set(ldapGroupIdsCurrentlyPartOf);
|
||||
|
||||
for await (const group of groupsToBePartOf) {
|
||||
if (!userGroupMembershipGroupIdsSet.has(group.id)) {
|
||||
// add user to group that they should be part of
|
||||
await addUsersToGroupByUserIds({
|
||||
group,
|
||||
userIds: [newUser.id],
|
||||
userDAL,
|
||||
userGroupMembershipDAL,
|
||||
orgDAL,
|
||||
groupProjectDAL,
|
||||
projectKeyDAL,
|
||||
projectDAL,
|
||||
projectBotDAL,
|
||||
tx
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const groupsCurrentlyPartOf = await groupDAL.find({
|
||||
orgId,
|
||||
$in: {
|
||||
id: ldapGroupIdsCurrentlyPartOf
|
||||
}
|
||||
});
|
||||
|
||||
for await (const group of groupsCurrentlyPartOf) {
|
||||
if (!toBePartOfGroupIdsSet.has(group.id)) {
|
||||
// remove user from group that they should no longer be part of
|
||||
await removeUsersFromGroupByUserIds({
|
||||
group,
|
||||
userIds: [newUser.id],
|
||||
userDAL,
|
||||
userGroupMembershipDAL,
|
||||
groupProjectDAL,
|
||||
projectKeyDAL,
|
||||
tx
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return newUser;
|
||||
});
|
||||
const user = await userDAL.findOne({ id: userAlias.userId });
|
||||
|
||||
const isUserCompleted = Boolean(user.isAccepted);
|
||||
|
||||
@ -564,116 +424,6 @@ export const ldapConfigServiceFactory = ({
|
||||
return { isUserCompleted, providerAuthToken };
|
||||
};
|
||||
|
||||
const getLdapGroupMaps = async ({
|
||||
ldapConfigId,
|
||||
actor,
|
||||
actorId,
|
||||
orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TGetLdapGroupMapsDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Ldap);
|
||||
|
||||
const ldapConfig = await ldapConfigDAL.findOne({
|
||||
id: ldapConfigId,
|
||||
orgId
|
||||
});
|
||||
|
||||
if (!ldapConfig) throw new BadRequestError({ message: "Failed to find organization LDAP data" });
|
||||
|
||||
const groupMaps = await ldapGroupMapDAL.findLdapGroupMapsByLdapConfigId(ldapConfigId);
|
||||
|
||||
return groupMaps;
|
||||
};
|
||||
|
||||
const createLdapGroupMap = async ({
|
||||
ldapConfigId,
|
||||
ldapGroupCN,
|
||||
groupSlug,
|
||||
actor,
|
||||
actorId,
|
||||
orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TCreateLdapGroupMapDTO) => {
|
||||
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 create LDAP group map due to plan restriction. Upgrade plan to create LDAP group map."
|
||||
});
|
||||
|
||||
const ldapConfig = await ldapConfigDAL.findOne({
|
||||
id: ldapConfigId,
|
||||
orgId
|
||||
});
|
||||
if (!ldapConfig) throw new BadRequestError({ message: "Failed to find organization LDAP data" });
|
||||
|
||||
const group = await groupDAL.findOne({ slug: groupSlug, orgId });
|
||||
if (!group) throw new BadRequestError({ message: "Failed to find group" });
|
||||
|
||||
const groupMap = await ldapGroupMapDAL.create({
|
||||
ldapConfigId,
|
||||
ldapGroupCN,
|
||||
groupId: group.id
|
||||
});
|
||||
|
||||
return groupMap;
|
||||
};
|
||||
|
||||
const deleteLdapGroupMap = async ({
|
||||
ldapConfigId,
|
||||
ldapGroupMapId,
|
||||
actor,
|
||||
actorId,
|
||||
orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TDeleteLdapGroupMapDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Ldap);
|
||||
|
||||
const plan = await licenseService.getPlan(orgId);
|
||||
if (!plan.ldap)
|
||||
throw new BadRequestError({
|
||||
message: "Failed to delete LDAP group map due to plan restriction. Upgrade plan to delete LDAP group map."
|
||||
});
|
||||
|
||||
const ldapConfig = await ldapConfigDAL.findOne({
|
||||
id: ldapConfigId,
|
||||
orgId
|
||||
});
|
||||
|
||||
if (!ldapConfig) throw new BadRequestError({ message: "Failed to find organization LDAP data" });
|
||||
|
||||
const [deletedGroupMap] = await ldapGroupMapDAL.delete({
|
||||
ldapConfigId: ldapConfig.id,
|
||||
id: ldapGroupMapId
|
||||
});
|
||||
|
||||
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,
|
||||
@ -681,10 +431,6 @@ export const ldapConfigServiceFactory = ({
|
||||
getLdapCfg,
|
||||
// getLdapPassportOpts,
|
||||
ldapLogin,
|
||||
bootLdap,
|
||||
getLdapGroupMaps,
|
||||
createLdapGroupMap,
|
||||
deleteLdapGroupMap,
|
||||
testLDAPConnection
|
||||
bootLdap
|
||||
};
|
||||
};
|
||||
|
@ -1,18 +1,5 @@
|
||||
import { TOrgPermission } from "@app/lib/types";
|
||||
|
||||
export type TLDAPConfig = {
|
||||
id: string;
|
||||
organization: string;
|
||||
isActive: boolean;
|
||||
url: string;
|
||||
bindDN: string;
|
||||
bindPass: string;
|
||||
searchBase: string;
|
||||
groupSearchBase: string;
|
||||
groupSearchFilter: string;
|
||||
caCert: string;
|
||||
};
|
||||
|
||||
export type TCreateLdapCfgDTO = {
|
||||
orgId: string;
|
||||
isActive: boolean;
|
||||
@ -20,9 +7,6 @@ export type TCreateLdapCfgDTO = {
|
||||
bindDN: string;
|
||||
bindPass: string;
|
||||
searchBase: string;
|
||||
searchFilter: string;
|
||||
groupSearchBase: string;
|
||||
groupSearchFilter: string;
|
||||
caCert: string;
|
||||
} & TOrgPermission;
|
||||
|
||||
@ -34,9 +18,6 @@ export type TUpdateLdapCfgDTO = {
|
||||
bindDN: string;
|
||||
bindPass: string;
|
||||
searchBase: string;
|
||||
searchFilter: string;
|
||||
groupSearchBase: string;
|
||||
groupSearchFilter: string;
|
||||
caCert: string;
|
||||
}> &
|
||||
TOrgPermission;
|
||||
@ -46,35 +27,11 @@ export type TGetLdapCfgDTO = {
|
||||
} & TOrgPermission;
|
||||
|
||||
export type TLdapLoginDTO = {
|
||||
ldapConfigId: string;
|
||||
externalId: string;
|
||||
username: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
emails: string[];
|
||||
orgId: string;
|
||||
groups?: {
|
||||
dn: string;
|
||||
cn: string;
|
||||
}[];
|
||||
relayState?: string;
|
||||
};
|
||||
|
||||
export type TGetLdapGroupMapsDTO = {
|
||||
ldapConfigId: string;
|
||||
} & TOrgPermission;
|
||||
|
||||
export type TCreateLdapGroupMapDTO = {
|
||||
ldapConfigId: string;
|
||||
ldapGroupCN: string;
|
||||
groupSlug: string;
|
||||
} & TOrgPermission;
|
||||
|
||||
export type TDeleteLdapGroupMapDTO = {
|
||||
ldapConfigId: string;
|
||||
ldapGroupMapId: string;
|
||||
} & TOrgPermission;
|
||||
|
||||
export type TTestLdapConnectionDTO = {
|
||||
ldapConfigId: string;
|
||||
} & TOrgPermission;
|
||||
|
@ -1,119 +0,0 @@
|
||||
import ldapjs from "ldapjs";
|
||||
|
||||
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,
|
||||
base: string
|
||||
): Promise<{ dn: string; cn: string }[]> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const ldapClient = ldapjs.createClient({
|
||||
url: ldapConfig.url,
|
||||
bindDN: ldapConfig.bindDN,
|
||||
bindCredentials: ldapConfig.bindPass,
|
||||
...(ldapConfig.caCert !== ""
|
||||
? {
|
||||
tlsOptions: {
|
||||
ca: [ldapConfig.caCert]
|
||||
}
|
||||
}
|
||||
: {})
|
||||
});
|
||||
|
||||
ldapClient.search(
|
||||
base,
|
||||
{
|
||||
filter,
|
||||
scope: "sub"
|
||||
},
|
||||
(err, res) => {
|
||||
if (err) {
|
||||
ldapClient.unbind();
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
const groups: { dn: string; cn: string }[] = [];
|
||||
|
||||
res.on("searchEntry", (entry) => {
|
||||
const dn = entry.dn.toString();
|
||||
const regex = /cn=([^,]+)/;
|
||||
const match = dn.match(regex);
|
||||
// parse the cn from the dn
|
||||
const cn = (match && match[1]) as string;
|
||||
|
||||
groups.push({ dn, cn });
|
||||
});
|
||||
res.on("error", (error) => {
|
||||
ldapClient.unbind();
|
||||
reject(error);
|
||||
});
|
||||
res.on("end", () => {
|
||||
ldapClient.unbind();
|
||||
resolve(groups);
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
};
|
@ -1,41 +0,0 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||
|
||||
export type TLdapGroupMapDALFactory = ReturnType<typeof ldapGroupMapDALFactory>;
|
||||
|
||||
export const ldapGroupMapDALFactory = (db: TDbClient) => {
|
||||
const ldapGroupMapOrm = ormify(db, TableName.LdapGroupMap);
|
||||
|
||||
const findLdapGroupMapsByLdapConfigId = async (ldapConfigId: string) => {
|
||||
try {
|
||||
const docs = await db(TableName.LdapGroupMap)
|
||||
.where(`${TableName.LdapGroupMap}.ldapConfigId`, ldapConfigId)
|
||||
.join(TableName.Groups, `${TableName.LdapGroupMap}.groupId`, `${TableName.Groups}.id`)
|
||||
.select(selectAllTableCols(TableName.LdapGroupMap))
|
||||
.select(
|
||||
db.ref("id").withSchema(TableName.Groups).as("groupId"),
|
||||
db.ref("name").withSchema(TableName.Groups).as("groupName"),
|
||||
db.ref("slug").withSchema(TableName.Groups).as("groupSlug")
|
||||
);
|
||||
|
||||
return docs.map((doc) => {
|
||||
return {
|
||||
id: doc.id,
|
||||
ldapConfigId: doc.ldapConfigId,
|
||||
ldapGroupCN: doc.ldapGroupCN,
|
||||
group: {
|
||||
id: doc.groupId,
|
||||
name: doc.groupName,
|
||||
slug: doc.groupSlug
|
||||
}
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "findGroupMaps" });
|
||||
}
|
||||
};
|
||||
|
||||
return { ...ldapGroupMapOrm, findLdapGroupMapsByLdapConfigId };
|
||||
};
|
@ -340,12 +340,11 @@ export const samlConfigServiceFactory = ({
|
||||
orgId,
|
||||
inviteEmail: email,
|
||||
role: OrgMembershipRole.Member,
|
||||
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
|
||||
status: OrgMembershipStatus.Accepted
|
||||
},
|
||||
tx
|
||||
);
|
||||
// Only update the membership to Accepted if the user account is already completed.
|
||||
} else if (orgMembership.status === OrgMembershipStatus.Invited && user.isAccepted) {
|
||||
} else if (orgMembership.status === OrgMembershipStatus.Invited) {
|
||||
await orgDAL.updateMembershipById(
|
||||
orgMembership.id,
|
||||
{
|
||||
|
@ -18,7 +18,6 @@ import { identityProjectAdditionalPrivilegeDALFactory } from "@app/ee/services/i
|
||||
import { identityProjectAdditionalPrivilegeServiceFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service";
|
||||
import { ldapConfigDALFactory } from "@app/ee/services/ldap-config/ldap-config-dal";
|
||||
import { ldapConfigServiceFactory } from "@app/ee/services/ldap-config/ldap-config-service";
|
||||
import { ldapGroupMapDALFactory } from "@app/ee/services/ldap-config/ldap-group-map-dal";
|
||||
import { licenseDALFactory } from "@app/ee/services/license/license-dal";
|
||||
import { licenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
import { permissionDALFactory } from "@app/ee/services/permission/permission-dal";
|
||||
@ -201,7 +200,6 @@ export const registerRoutes = async (
|
||||
const samlConfigDAL = samlConfigDALFactory(db);
|
||||
const scimDAL = scimDALFactory(db);
|
||||
const ldapConfigDAL = ldapConfigDALFactory(db);
|
||||
const ldapGroupMapDAL = ldapGroupMapDALFactory(db);
|
||||
const sapApproverDAL = secretApprovalPolicyApproverDALFactory(db);
|
||||
const secretApprovalPolicyDAL = secretApprovalPolicyDALFactory(db);
|
||||
const secretApprovalRequestDAL = secretApprovalRequestDALFactory(db);
|
||||
@ -302,15 +300,8 @@ export const registerRoutes = async (
|
||||
|
||||
const ldapService = ldapConfigServiceFactory({
|
||||
ldapConfigDAL,
|
||||
ldapGroupMapDAL,
|
||||
orgDAL,
|
||||
orgBotDAL,
|
||||
groupDAL,
|
||||
groupProjectDAL,
|
||||
projectKeyDAL,
|
||||
projectDAL,
|
||||
projectBotDAL,
|
||||
userGroupMembershipDAL,
|
||||
userDAL,
|
||||
userAliasDAL,
|
||||
permissionService,
|
||||
|
@ -1,4 +1,3 @@
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
import { z } from "zod";
|
||||
|
||||
import { ProjectEnvironmentsSchema } from "@app/db/schemas";
|
||||
@ -27,13 +26,7 @@ export const registerProjectEnvRouter = async (server: FastifyZodProvider) => {
|
||||
}),
|
||||
body: z.object({
|
||||
name: z.string().trim().describe(ENVIRONMENTS.CREATE.name),
|
||||
slug: z
|
||||
.string()
|
||||
.trim()
|
||||
.refine((v) => slugify(v) === v, {
|
||||
message: "Slug must be a valid slug"
|
||||
})
|
||||
.describe(ENVIRONMENTS.CREATE.slug)
|
||||
slug: z.string().trim().describe(ENVIRONMENTS.CREATE.slug)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
@ -91,14 +84,7 @@ export const registerProjectEnvRouter = async (server: FastifyZodProvider) => {
|
||||
id: z.string().trim().describe(ENVIRONMENTS.UPDATE.id)
|
||||
}),
|
||||
body: z.object({
|
||||
slug: z
|
||||
.string()
|
||||
.trim()
|
||||
.optional()
|
||||
.refine((v) => !v || slugify(v) === v, {
|
||||
message: "Slug must be a valid slug"
|
||||
})
|
||||
.describe(ENVIRONMENTS.UPDATE.slug),
|
||||
slug: z.string().trim().optional().describe(ENVIRONMENTS.UPDATE.slug),
|
||||
name: z.string().trim().optional().describe(ENVIRONMENTS.UPDATE.name),
|
||||
position: z.number().optional().describe(ENVIRONMENTS.UPDATE.position)
|
||||
}),
|
||||
|
@ -191,7 +191,7 @@ export const authLoginServiceFactory = ({
|
||||
const decodedProviderToken = validateProviderAuthToken(providerAuthToken, email);
|
||||
|
||||
authMethod = decodedProviderToken.authMethod;
|
||||
if ((isAuthMethodSaml(authMethod) || authMethod === AuthMethod.LDAP) && decodedProviderToken.orgId) {
|
||||
if (isAuthMethodSaml(authMethod) && decodedProviderToken.orgId) {
|
||||
organizationId = decodedProviderToken.orgId;
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,6 @@ 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";
|
||||
@ -140,11 +139,9 @@ export const authSignupServiceFactory = ({
|
||||
throw new Error("Failed to complete account for complete user");
|
||||
}
|
||||
|
||||
let organizationId: string | null = null;
|
||||
let authMethod: AuthMethod | null = null;
|
||||
let organizationId;
|
||||
if (providerAuthToken) {
|
||||
const { orgId, authMethod: userAuthMethod } = validateProviderAuthToken(providerAuthToken, user.username);
|
||||
authMethod = userAuthMethod;
|
||||
const { orgId } = validateProviderAuthToken(providerAuthToken, user.username);
|
||||
organizationId = orgId;
|
||||
} else {
|
||||
validateSignUpAuthorization(authorization, user.id);
|
||||
@ -168,26 +165,6 @@ 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 };
|
||||
});
|
||||
|
||||
|
@ -572,14 +572,14 @@ export const integrationAuthServiceFactory = ({
|
||||
const response = keys
|
||||
.Keys!.map((key) => {
|
||||
const keyAlias = aliases.Aliases!.find((alias) => key.KeyId === alias.TargetKeyId);
|
||||
if (!keyAlias?.AliasName?.includes("alias/aws/")) {
|
||||
if (!keyAlias?.AliasName?.includes("alias/aws/") || keyAlias?.AliasName?.includes("alias/aws/secretsmanager")) {
|
||||
return { id: String(key.KeyId), alias: String(keyAlias?.AliasName || key.KeyId) };
|
||||
}
|
||||
return { id: "null", alias: "null" };
|
||||
})
|
||||
.filter((elem) => elem.id !== "null");
|
||||
|
||||
return [...response, { id: "null", alias: "default" }];
|
||||
return response;
|
||||
};
|
||||
|
||||
const getQoveryProjects = async ({
|
||||
|
@ -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,
|
||||
@ -477,29 +477,24 @@ const syncSecretsAWSParameterStore = async ({
|
||||
}),
|
||||
{} as Record<string, AWS.SSM.Parameter>
|
||||
);
|
||||
|
||||
// Identify secrets to create
|
||||
await Promise.all(
|
||||
Object.keys(secrets).map(async (key) => {
|
||||
if (!(key in awsParameterStoreSecretsObj)) {
|
||||
// case: secret does not exist in AWS parameter store
|
||||
// -> create secret
|
||||
if (secrets[key].value) {
|
||||
await ssm
|
||||
.putParameter({
|
||||
Name: `${integration.path}${key}`,
|
||||
Type: "SecureString",
|
||||
Value: secrets[key].value,
|
||||
KeyId: metadata.kmsKeyId ? metadata.kmsKeyId : undefined,
|
||||
// Overwrite: true,
|
||||
Tags: metadata.secretAWSTag
|
||||
? metadata.secretAWSTag.map((tag: { key: string; value: string }) => ({
|
||||
Key: tag.key,
|
||||
Value: tag.value
|
||||
}))
|
||||
: []
|
||||
})
|
||||
.promise();
|
||||
}
|
||||
await ssm
|
||||
.putParameter({
|
||||
Name: `${integration.path}${key}`,
|
||||
Type: "SecureString",
|
||||
Value: secrets[key].value,
|
||||
// Overwrite: true,
|
||||
Tags: metadata.secretAWSTag
|
||||
? metadata.secretAWSTag.map((tag: { key: string; value: string }) => ({ Key: tag.key, Value: tag.value }))
|
||||
: []
|
||||
})
|
||||
.promise();
|
||||
// case: secret exists in AWS parameter store
|
||||
} else if (awsParameterStoreSecretsObj[key].Value !== secrets[key].value) {
|
||||
// case: secret value doesn't match one in AWS parameter store
|
||||
@ -549,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;
|
||||
|
||||
|
@ -1,66 +1,33 @@
|
||||
import { SecretType, TSecretImports, TSecrets } from "@app/db/schemas";
|
||||
import { SecretType, TSecretImports } 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: possibleCyclicImports,
|
||||
allowedImports,
|
||||
folderDAL,
|
||||
secretDAL,
|
||||
secretImportDAL,
|
||||
depth = 0,
|
||||
cyclicDetector = new Set()
|
||||
secretDAL
|
||||
}: {
|
||||
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>;
|
||||
}) => {
|
||||
// avoid going more than a depth
|
||||
if (depth >= LEVEL_BREAK) return [];
|
||||
|
||||
const allowedImports = possibleCyclicImports.filter(
|
||||
({ importPath, importEnv }) => !cyclicDetector.has(getImportUniqKey(importEnv.slug, importPath))
|
||||
const importedFolders = await folderDAL.findByManySecretPath(
|
||||
allowedImports.map(({ importEnv, importPath }) => ({
|
||||
envId: importEnv.id,
|
||||
secretPath: importPath
|
||||
}))
|
||||
);
|
||||
|
||||
const importedFolders = (
|
||||
await folderDAL.findByManySecretPath(
|
||||
allowedImports.map(({ importEnv, importPath }) => ({
|
||||
envId: importEnv.id,
|
||||
secretPath: importPath
|
||||
}))
|
||||
)
|
||||
).filter(Boolean); // remove undefined ones
|
||||
if (!importedFolders.length) {
|
||||
const folderIds = importedFolders.map((el) => el?.id).filter(Boolean) as string[];
|
||||
if (!folderIds.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: importedFolderIds },
|
||||
$in: { folderId: folderIds },
|
||||
type: SecretType.Shared
|
||||
},
|
||||
{
|
||||
@ -68,50 +35,18 @@ export const fnSecretsFromImports = async ({
|
||||
}
|
||||
);
|
||||
|
||||
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,
|
||||
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,
|
||||
environment: importEnv.slug,
|
||||
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;
|
||||
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.
|
||||
}))
|
||||
}));
|
||||
};
|
||||
|
@ -290,7 +290,7 @@ export const secretImportServiceFactory = ({
|
||||
})
|
||||
)
|
||||
);
|
||||
return fnSecretsFromImports({ allowedImports, folderDAL, secretDAL, secretImportDAL });
|
||||
return fnSecretsFromImports({ allowedImports, folderDAL, secretDAL });
|
||||
};
|
||||
|
||||
return {
|
||||
|
@ -525,8 +525,7 @@ export const secretServiceFactory = ({
|
||||
const importedSecrets = await fnSecretsFromImports({
|
||||
allowedImports,
|
||||
secretDAL,
|
||||
folderDAL,
|
||||
secretImportDAL
|
||||
folderDAL
|
||||
});
|
||||
|
||||
return {
|
||||
@ -631,8 +630,7 @@ export const secretServiceFactory = ({
|
||||
const importedSecrets = await fnSecretsFromImports({
|
||||
allowedImports,
|
||||
secretDAL,
|
||||
folderDAL,
|
||||
secretImportDAL
|
||||
folderDAL
|
||||
});
|
||||
for (let i = importedSecrets.length - 1; i >= 0; i -= 1) {
|
||||
for (let j = 0; j < importedSecrets[i].secrets.length; j += 1) {
|
||||
|
@ -22,6 +22,10 @@ var folderCmd = &cobra.Command{
|
||||
var getCmd = &cobra.Command{
|
||||
Use: "get",
|
||||
Short: "Get folders in a directory",
|
||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||
util.RequireLocalWorkspaceFile()
|
||||
util.RequireLogin()
|
||||
},
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
|
||||
environmentName, _ := cmd.Flags().GetString("env")
|
||||
|
@ -7,7 +7,6 @@ import (
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
@ -205,10 +204,8 @@ var secretsSetCmd = &cobra.Command{
|
||||
// decrypt workspace key
|
||||
plainTextEncryptionKey := crypto.DecryptAsymmetric(encryptedWorkspaceKey, encryptedWorkspaceKeyNonce, encryptedWorkspaceKeySenderPublicKey, currentUsersPrivateKey)
|
||||
|
||||
infisicalTokenEnv := os.Getenv(util.INFISICAL_TOKEN_NAME)
|
||||
|
||||
// pull current secrets
|
||||
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, SecretsPath: secretsPath, InfisicalToken: infisicalTokenEnv}, "")
|
||||
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, SecretsPath: secretsPath}, "")
|
||||
if err != nil {
|
||||
util.HandleError(err, "unable to retrieve secrets")
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ package util
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/Infisical/infisical-merge/packages/api"
|
||||
@ -12,11 +13,13 @@ import (
|
||||
|
||||
func GetAllFolders(params models.GetAllFoldersParameters) ([]models.SingleFolder, error) {
|
||||
|
||||
if params.InfisicalToken == "" {
|
||||
params.InfisicalToken = os.Getenv(INFISICAL_TOKEN_NAME)
|
||||
}
|
||||
|
||||
var foldersToReturn []models.SingleFolder
|
||||
var folderErr error
|
||||
if params.InfisicalToken == "" && params.UniversalAuthAccessToken == "" {
|
||||
RequireLogin()
|
||||
RequireLocalWorkspaceFile()
|
||||
|
||||
log.Debug().Msg("GetAllFolders: Trying to fetch folders using logged in details")
|
||||
|
||||
|
@ -307,6 +307,10 @@ func FilterSecretsByTag(plainTextSecrets []models.SingleEnvironmentVariable, tag
|
||||
}
|
||||
|
||||
func GetAllEnvironmentVariables(params models.GetAllSecretsParameters, projectConfigFilePath string) ([]models.SingleEnvironmentVariable, error) {
|
||||
if params.InfisicalToken == "" {
|
||||
params.InfisicalToken = os.Getenv(INFISICAL_TOKEN_NAME)
|
||||
}
|
||||
|
||||
isConnected := CheckIsConnectedToInternet()
|
||||
var secretsToReturn []models.SingleEnvironmentVariable
|
||||
// var serviceTokenDetails api.GetServiceTokenDetailsResponse
|
||||
|
@ -1,78 +0,0 @@
|
||||
global
|
||||
maxconn 10000
|
||||
log stdout format raw local0
|
||||
|
||||
defaults
|
||||
log global
|
||||
mode tcp
|
||||
retries 3
|
||||
timeout client 30m
|
||||
timeout connect 10s
|
||||
timeout server 30m
|
||||
timeout check 5s
|
||||
|
||||
listen stats
|
||||
mode http
|
||||
bind *:7000
|
||||
stats enable
|
||||
stats uri /
|
||||
|
||||
resolvers hostdns
|
||||
nameserver dns 127.0.0.11:53
|
||||
resolve_retries 3
|
||||
timeout resolve 1s
|
||||
timeout retry 1s
|
||||
hold valid 5s
|
||||
|
||||
frontend master
|
||||
bind *:5433
|
||||
default_backend master_backend
|
||||
|
||||
frontend replicas
|
||||
bind *:5434
|
||||
default_backend replica_backend
|
||||
|
||||
|
||||
backend master_backend
|
||||
option httpchk GET /master
|
||||
http-check expect status 200
|
||||
default-server inter 3s fall 3 rise 2 on-marked-down shutdown-sessions
|
||||
server postgres-1 postgres-1:5432 check port 8008 resolvers hostdns
|
||||
server postgres-2 postgres-2:5432 check port 8008 resolvers hostdns
|
||||
server postgres-3 postgres-3:5432 check port 8008 resolvers hostdns
|
||||
|
||||
backend replica_backend
|
||||
option httpchk GET /replica
|
||||
http-check expect status 200
|
||||
default-server inter 3s fall 3 rise 2 on-marked-down shutdown-sessions
|
||||
server postgres-1 postgres-1:5432 check port 8008 resolvers hostdns
|
||||
server postgres-2 postgres-2:5432 check port 8008 resolvers hostdns
|
||||
server postgres-3 postgres-3:5432 check port 8008 resolvers hostdns
|
||||
|
||||
|
||||
frontend redis_frontend
|
||||
bind *:6379
|
||||
default_backend redis_backend
|
||||
|
||||
backend redis_backend
|
||||
option tcp-check
|
||||
tcp-check send AUTH\ 123456\r\n
|
||||
tcp-check expect string +OK
|
||||
tcp-check send PING\r\n
|
||||
tcp-check expect string +PONG
|
||||
tcp-check send info\ replication\r\n
|
||||
tcp-check expect string role:master
|
||||
tcp-check send QUIT\r\n
|
||||
tcp-check expect string +OK
|
||||
server redis_master redis_replica0:6379 check inter 1s
|
||||
server redis_replica1 redis_replica1:6379 check inter 1s
|
||||
server redis_replica2 redis_replica2:6379 check inter 1s
|
||||
|
||||
frontend infisical_frontend
|
||||
bind *:8080
|
||||
default_backend infisical_backend
|
||||
|
||||
backend infisical_backend
|
||||
option httpchk GET /api/status
|
||||
http-check expect status 200
|
||||
server infisical infisical:8080 check inter 1s
|
@ -1,259 +0,0 @@
|
||||
version: "3"
|
||||
|
||||
services:
|
||||
haproxy:
|
||||
image: haproxy:latest
|
||||
ports:
|
||||
- '7001:7000'
|
||||
- '5002:5433'
|
||||
- '5003:5434'
|
||||
- '6379:6379'
|
||||
- '8080:8080'
|
||||
networks:
|
||||
- infisical
|
||||
configs:
|
||||
- source: haproxy-config
|
||||
target: /usr/local/etc/haproxy/haproxy.cfg
|
||||
deploy:
|
||||
placement:
|
||||
constraints:
|
||||
- node.labels.name == node1
|
||||
|
||||
infisical:
|
||||
container_name: infisical-backend
|
||||
image: infisical/infisical:latest-postgres
|
||||
env_file: .env
|
||||
ports:
|
||||
- 80:8080
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
networks:
|
||||
- infisical
|
||||
secrets:
|
||||
- env_file
|
||||
|
||||
etcd1:
|
||||
image: ghcr.io/zalando/spilo-16:3.2-p2
|
||||
networks:
|
||||
- infisical
|
||||
environment:
|
||||
ETCD_UNSUPPORTED_ARCH: arm64
|
||||
container_name: demo-etcd1
|
||||
deploy:
|
||||
placement:
|
||||
constraints:
|
||||
- node.labels.name == node1
|
||||
hostname: etcd1
|
||||
command: |
|
||||
etcd --name etcd1
|
||||
--listen-client-urls http://0.0.0.0:2379
|
||||
--listen-peer-urls=http://0.0.0.0:2380
|
||||
--advertise-client-urls http://etcd1:2379
|
||||
--initial-cluster=etcd1=http://etcd1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380
|
||||
--initial-advertise-peer-urls=http://etcd1:2380
|
||||
--initial-cluster-state=new
|
||||
|
||||
etcd2:
|
||||
image: ghcr.io/zalando/spilo-16:3.2-p2
|
||||
networks:
|
||||
- infisical
|
||||
environment:
|
||||
ETCD_UNSUPPORTED_ARCH: arm64
|
||||
container_name: demo-etcd2
|
||||
hostname: etcd2
|
||||
deploy:
|
||||
placement:
|
||||
constraints:
|
||||
- node.labels.name == node2
|
||||
command: |
|
||||
etcd --name etcd2
|
||||
--listen-client-urls http://0.0.0.0:2379
|
||||
--listen-peer-urls=http://0.0.0.0:2380
|
||||
--advertise-client-urls http://etcd2:2379
|
||||
--initial-cluster=etcd1=http://etcd1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380
|
||||
--initial-advertise-peer-urls=http://etcd2:2380
|
||||
--initial-cluster-state=new
|
||||
|
||||
etcd3:
|
||||
image: ghcr.io/zalando/spilo-16:3.2-p2
|
||||
networks:
|
||||
- infisical
|
||||
environment:
|
||||
ETCD_UNSUPPORTED_ARCH: arm64
|
||||
container_name: demo-etcd3
|
||||
hostname: etcd3
|
||||
deploy:
|
||||
placement:
|
||||
constraints:
|
||||
- node.labels.name == node3
|
||||
command: |
|
||||
etcd --name etcd3
|
||||
--listen-client-urls http://0.0.0.0:2379
|
||||
--listen-peer-urls=http://0.0.0.0:2380
|
||||
--advertise-client-urls http://etcd3:2379
|
||||
--initial-cluster=etcd1=http://etcd1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380
|
||||
--initial-advertise-peer-urls=http://etcd3:2380
|
||||
--initial-cluster-state=new
|
||||
|
||||
spolo1:
|
||||
image: ghcr.io/zalando/spilo-16:3.2-p2
|
||||
container_name: postgres-1
|
||||
networks:
|
||||
- infisical
|
||||
hostname: postgres-1
|
||||
environment:
|
||||
ETCD_HOSTS: etcd1:2379,etcd2:2379,etcd3:2379
|
||||
SCOPE: infisical
|
||||
volumes:
|
||||
- postgres_data1:/home/postgres/pgdata
|
||||
deploy:
|
||||
placement:
|
||||
constraints:
|
||||
- node.labels.name == node1
|
||||
|
||||
spolo2:
|
||||
image: ghcr.io/zalando/spilo-16:3.2-p2
|
||||
container_name: postgres-2
|
||||
networks:
|
||||
- infisical
|
||||
hostname: postgres-2
|
||||
environment:
|
||||
ETCD_HOSTS: etcd1:2379,etcd2:2379,etcd3:2379
|
||||
SCOPE: infisical
|
||||
volumes:
|
||||
- postgres_data2:/home/postgres/pgdata
|
||||
deploy:
|
||||
placement:
|
||||
constraints:
|
||||
- node.labels.name == node2
|
||||
|
||||
spolo3:
|
||||
image: ghcr.io/zalando/spilo-16:3.2-p2
|
||||
container_name: postgres-3
|
||||
networks:
|
||||
- infisical
|
||||
hostname: postgres-3
|
||||
environment:
|
||||
ETCD_HOSTS: etcd1:2379,etcd2:2379,etcd3:2379
|
||||
SCOPE: infisical
|
||||
volumes:
|
||||
- postgres_data3:/home/postgres/pgdata
|
||||
deploy:
|
||||
placement:
|
||||
constraints:
|
||||
- node.labels.name == node3
|
||||
|
||||
|
||||
redis_replica0:
|
||||
image: bitnami/redis:6.2.10
|
||||
environment:
|
||||
- REDIS_REPLICATION_MODE=master
|
||||
- REDIS_PASSWORD=123456
|
||||
networks:
|
||||
- infisical
|
||||
deploy:
|
||||
placement:
|
||||
constraints:
|
||||
- node.labels.name == node1
|
||||
|
||||
redis_replica1:
|
||||
image: bitnami/redis:6.2.10
|
||||
environment:
|
||||
- REDIS_REPLICATION_MODE=slave
|
||||
- REDIS_MASTER_HOST=redis_replica0
|
||||
- REDIS_MASTER_PORT_NUMBER=6379
|
||||
- REDIS_MASTER_PASSWORD=123456
|
||||
- REDIS_PASSWORD=123456
|
||||
networks:
|
||||
- infisical
|
||||
deploy:
|
||||
placement:
|
||||
constraints:
|
||||
- node.labels.name == node2
|
||||
|
||||
redis_replica2:
|
||||
image: bitnami/redis:6.2.10
|
||||
environment:
|
||||
- REDIS_REPLICATION_MODE=slave
|
||||
- REDIS_MASTER_HOST=redis_replica0
|
||||
- REDIS_MASTER_PORT_NUMBER=6379
|
||||
- REDIS_MASTER_PASSWORD=123456
|
||||
- REDIS_PASSWORD=123456
|
||||
networks:
|
||||
- infisical
|
||||
deploy:
|
||||
placement:
|
||||
constraints:
|
||||
- node.labels.name == node3
|
||||
|
||||
redis_sentinel1:
|
||||
image: bitnami/redis-sentinel:6.2.10
|
||||
environment:
|
||||
- REDIS_SENTINEL_QUORUM=2
|
||||
- REDIS_SENTINEL_DOWN_AFTER_MILLISECONDS=5000
|
||||
- REDIS_SENTINEL_FAILOVER_TIMEOUT=60000
|
||||
- REDIS_SENTINEL_PORT_NUMBER=26379
|
||||
- REDIS_MASTER_HOST=redis_replica1
|
||||
- REDIS_MASTER_PORT_NUMBER=6379
|
||||
- REDIS_MASTER_PASSWORD=123456
|
||||
networks:
|
||||
- infisical
|
||||
deploy:
|
||||
placement:
|
||||
constraints:
|
||||
- node.labels.name == node1
|
||||
|
||||
redis_sentinel2:
|
||||
image: bitnami/redis-sentinel:6.2.10
|
||||
environment:
|
||||
- REDIS_SENTINEL_QUORUM=2
|
||||
- REDIS_SENTINEL_DOWN_AFTER_MILLISECONDS=5000
|
||||
- REDIS_SENTINEL_FAILOVER_TIMEOUT=60000
|
||||
- REDIS_SENTINEL_PORT_NUMBER=26379
|
||||
- REDIS_MASTER_HOST=redis_replica1
|
||||
- REDIS_MASTER_PORT_NUMBER=6379
|
||||
- REDIS_MASTER_PASSWORD=123456
|
||||
networks:
|
||||
- infisical
|
||||
deploy:
|
||||
placement:
|
||||
constraints:
|
||||
- node.labels.name == node2
|
||||
|
||||
redis_sentinel3:
|
||||
image: bitnami/redis-sentinel:6.2.10
|
||||
environment:
|
||||
- REDIS_SENTINEL_QUORUM=2
|
||||
- REDIS_SENTINEL_DOWN_AFTER_MILLISECONDS=5000
|
||||
- REDIS_SENTINEL_FAILOVER_TIMEOUT=60000
|
||||
- REDIS_SENTINEL_PORT_NUMBER=26379
|
||||
- REDIS_MASTER_HOST=redis_replica1
|
||||
- REDIS_MASTER_PORT_NUMBER=6379
|
||||
- REDIS_MASTER_PASSWORD=123456
|
||||
networks:
|
||||
- infisical
|
||||
deploy:
|
||||
placement:
|
||||
constraints:
|
||||
- node.labels.name == node3
|
||||
|
||||
networks:
|
||||
infisical:
|
||||
|
||||
|
||||
volumes:
|
||||
postgres_data1:
|
||||
postgres_data2:
|
||||
postgres_data3:
|
||||
postgres_data4:
|
||||
redis0:
|
||||
redis1:
|
||||
redis2:
|
||||
|
||||
configs:
|
||||
haproxy-config:
|
||||
file: ./haproxy.cfg
|
||||
|
||||
secrets:
|
||||
env_file:
|
||||
file: .env
|
@ -1,14 +1,13 @@
|
||||
---
|
||||
title: "Dynamic Secrets"
|
||||
sidebarTitle: "Overview"
|
||||
title: "Overview"
|
||||
description: "Learn how to generate secrets dynamically on-demand."
|
||||
---
|
||||
|
||||
## Introduction
|
||||
|
||||
Contrary to static key-value secrets, which require manual input of data into the secure Infisical storage, **dynamic secrets are generated on-demand upon access**.
|
||||
Contrary to static key-value secrets, which require manual input of data into the secure Infisical storage, dynamic secrets are generated on-demand upon access.
|
||||
|
||||
**Dynamic secrets are unique to every identity using them**. Such secrets come are generated only at the moment they are retrieved, eliminating the possibility of theft or reuse by another identity. Thanks to Infisical's integrated revocation capabilities, dynamic secrets can be promptly invalidated post-use, significantly reducing their lifespan.
|
||||
Dynamic secrets are unique to every identity using them. Such secrets come are generated only at the moment they are retrieved, eliminating the possibility of theft or reuse by another identity. Thanks to Infisical's integrated revocation capabilities, dynamic secrets can be promptly invalidated post-use, significantly reducing their lifespan.
|
||||
|
||||
## Benefits of Dynamic Secrets
|
||||
|
||||
@ -29,6 +28,3 @@ Dynamic secrets are particularly useful in environments with stringent security
|
||||
## Infisical Dynamic Secret Templates
|
||||
|
||||
1. [PostgreSQL](./postgresql)
|
||||
2. [MySQL](./mysql)
|
||||
3. [Cassandra](./cassandra)
|
||||
4. [Oracle](./oracle)
|
||||
|
36
docs/documentation/platform/ldap.mdx
Normal file
@ -0,0 +1,36 @@
|
||||
---
|
||||
title: "LDAP"
|
||||
description: "Log in to Infisical with LDAP"
|
||||
---
|
||||
|
||||
<Info>
|
||||
LDAP is a paid feature.
|
||||
|
||||
If you're using Infisical Cloud, then it is available under the **Enterprise Tier**. If you're self-hosting Infisical,
|
||||
then you should contact sales@infisical.com to purchase an enterprise license to use it.
|
||||
</Info>
|
||||
|
||||
You can configure your organization in Infisical to have members authenticate with the platform via [LDAP](https://en.wikipedia.org/wiki/Lightweight_Directory_Access_Protocol).
|
||||
|
||||
<Steps>
|
||||
<Step title="Prepare the LDAP configuration in Infisical">
|
||||
In Infisical, head to your Organization Settings > Authentication > LDAP Configuration and select **Set up LDAP**.
|
||||
|
||||
Next, input your LDAP server settings.
|
||||
|
||||

|
||||
|
||||
Here's some guidance for each field:
|
||||
|
||||
- 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=example,dc=com`
|
||||
- CA Certificate: The CA certificate to use when verifying the LDAP server certificate.
|
||||
</Step>
|
||||
<Step title="Enable LDAP in Infisical">
|
||||
Enabling LDAP allows members in your organization to log into Infisical via LDAP.
|
||||
|
||||

|
||||
</Step>
|
||||
</Steps>
|
@ -4,17 +4,16 @@ description: "Learn how to log in to Infisical with LDAP."
|
||||
---
|
||||
|
||||
<Info>
|
||||
LDAP is a paid feature. If you're using Infisical Cloud, then it is available
|
||||
under the **Enterprise Tier**. If you're self-hosting Infisical, then you
|
||||
should contact sales@infisical.com to purchase an enterprise license to use
|
||||
it.
|
||||
LDAP is a paid feature.
|
||||
If you're using Infisical Cloud, then it is available under the **Enterprise Tier**. If you're self-hosting Infisical,
|
||||
then you should contact sales@infisical.com to purchase an enterprise license to use it.
|
||||
</Info>
|
||||
|
||||
You can configure your organization in Infisical to have members authenticate with the platform via [LDAP](https://en.wikipedia.org/wiki/Lightweight_Directory_Access_Protocol)
|
||||
|
||||
<Steps>
|
||||
<Step title="Prepare the LDAP configuration in Infisical">
|
||||
In Infisical, head to your Organization Settings > Security > LDAP and select **Manage**.
|
||||
In Infisical, head to your Organization Settings > Authentication > LDAP Configuration and select **Set up LDAP**.
|
||||
|
||||
Next, input your LDAP server settings.
|
||||
|
||||
@ -25,50 +24,11 @@ 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.
|
||||
- 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)(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.
|
||||
- Search Base / User DN: Base DN under which to perform user search such as `ou=Users,dc=example,dc=com`
|
||||
- CA Certificate: The CA certificate to use when verifying the LDAP server certificate.
|
||||
|
||||
<Note>
|
||||
The **Group Search Base / Group DN** and **Group Filter** fields are both required if you wish to sync LDAP groups to Infisical.
|
||||
</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.
|
||||
|
||||

|
||||
</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.
|
||||
|
||||

|
||||
|
||||
Group mappings ensure that users who log into Infisical via LDAP are added to or removed from the Infisical group(s) that corresponds to the LDAP group(s) they are a member of.
|
||||
|
||||

|
||||
|
||||
Each group mapping consists of two parts:
|
||||
- LDAP Group CN: The common name of the LDAP group to map.
|
||||
- Infisical Group: The Infisical group to map the LDAP group to.
|
||||
|
||||
For example, suppose you want to automatically add a user who is part of the LDAP group with CN `Engineers` to the Infisical group `Engineers` when the user sets up their account with Infisical.
|
||||
|
||||
In this case, you would specify a mapping from the LDAP group with CN `Engineers` to the Infisical group `Engineers`.
|
||||
Now when the user logs into Infisical via LDAP, Infisical will check the LDAP groups that the user is a part of whilst referencing the group mappings you created earlier. Since the user is a member of the LDAP group with CN `Engineers`, they will be added to the Infisical group `Engineers`.
|
||||
In the future, if the user is no longer part of the LDAP group with CN `Engineers`, they will be removed from the Infisical group `Engineers` upon their next login.
|
||||
<Note>
|
||||
Prior to defining any group mappings, ensure that you've created the Infisical groups that you want to map the LDAP groups to.
|
||||
You can read more about creating (user) groups in Infisical [here](/documentation/platform/groups).
|
||||
</Note>
|
||||
</Step>
|
||||
<Step title="Enable LDAP in Infisical">
|
||||
Enabling LDAP allows members in your organization to log into Infisical via LDAP.
|
||||

|
||||
</Step>
|
||||
</Steps>
|
||||
</Steps>
|
@ -4,10 +4,9 @@ description: "Learn how to configure JumpCloud LDAP for authenticating into Infi
|
||||
---
|
||||
|
||||
<Info>
|
||||
LDAP is a paid feature. If you're using Infisical Cloud, then it is available
|
||||
under the **Enterprise Tier**. If you're self-hosting Infisical, then you
|
||||
should contact sales@infisical.com to purchase an enterprise license to use
|
||||
it.
|
||||
LDAP is a paid feature.
|
||||
If you're using Infisical Cloud, then it is available under the **Enterprise Tier**. If you're self-hosting Infisical,
|
||||
then you should contact sales@infisical.com to purchase an enterprise license to use it.
|
||||
</Info>
|
||||
|
||||
<Steps>
|
||||
@ -18,12 +17,13 @@ description: "Learn how to configure JumpCloud LDAP for authenticating into Infi
|
||||
When creating the user, input their **First Name**, **Last Name**, **Username** (required), **Company Email** (required), and **Description**.
|
||||
Also, create a password for the user.
|
||||
|
||||
Next, under User Security Settings and Permissions > Permission Settings, check the box next to **Enable as LDAP Bind DN**.
|
||||
Next, under User Security Settings and Permissions > Permission Settings, check the box next to **Enable as LDAP Bind DN**.
|
||||
|
||||

|
||||
|
||||
</Step>
|
||||
<Step title="Prepare the LDAP configuration in Infisical">
|
||||
In Infisical, head to your Organization Settings > Security > LDAP and select **Manage**.
|
||||
In Infisical, head to your Organization Settings > Authentication > LDAP Configuration and select **Set up LDAP**.
|
||||
|
||||
Next, input your JumpCloud LDAP server settings.
|
||||
|
||||
@ -34,57 +34,21 @@ 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.
|
||||
- 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)(member=uid={{.Username}},ou=Users,o=<your-org-id>,dc=jumpcloud,dc=com))`)
|
||||
- Search Base / User DN: Base DN under which to perform user search (`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>
|
||||
When filling out the **Bind DN** and **Bind Pass** fields, refer to the username and password of the user created in Step 1.
|
||||
|
||||
Also, for the **Bind DN** and **Search Base / User DN** fields, you'll want to use the organization ID that appears
|
||||
Also, for the **Bind DN** and **Search Base / User DN** fields, you'll want to use the organization ID that appears
|
||||
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.
|
||||
|
||||

|
||||
</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.
|
||||
|
||||

|
||||
|
||||
Group mappings ensure that users who log into Infisical via LDAP are added to or removed from the Infisical group(s) that corresponds to the LDAP group(s) they are a member of.
|
||||
|
||||

|
||||
|
||||
Each group mapping consists of two parts:
|
||||
- LDAP Group CN: The common name of the LDAP group to map.
|
||||
- Infisical Group: The Infisical group to map the LDAP group to.
|
||||
|
||||
For example, suppose you want to automatically add a user who is part of the LDAP group with CN `Engineers` to the Infisical group `Engineers` when the user sets up their account with Infisical.
|
||||
|
||||
In this case, you would specify a mapping from the LDAP group with CN `Engineers` to the Infisical group `Engineers`.
|
||||
Now when the user logs into Infisical via LDAP, Infisical will check the LDAP groups that the user is a part of whilst referencing the group mappings you created earlier. Since the user is a member of the LDAP group with CN `Engineers`, they will be added to the Infisical group `Engineers`.
|
||||
In the future, if the user is no longer part of the LDAP group with CN `Engineers`, they will be removed from the Infisical group `Engineers` upon their next login.
|
||||
<Note>
|
||||
Prior to defining any group mappings, ensure that you've created the Infisical groups that you want to map the LDAP groups to.
|
||||
You can read more about creating (user) groups in Infisical [here](/documentation/platform/groups).
|
||||
</Note>
|
||||
|
||||
</Step>
|
||||
<Step title="Enable LDAP in Infisical">
|
||||
Enabling LDAP allows members in your organization to log into Infisical via LDAP.
|
||||

|
||||
</Step>
|
||||
|
||||
</Steps>
|
||||
|
||||
Resources:
|
||||
|
||||
- [JumpCloud Cloud LDAP Guide](https://jumpcloud.com/support/use-cloud-ldap)
|
||||
- [JumpCloud Cloud LDAP Guide](https://jumpcloud.com/support/use-cloud-ldap)
|
Before Width: | Height: | Size: 131 KiB After Width: | Height: | Size: 181 KiB |
Before Width: | Height: | Size: 160 KiB After Width: | Height: | Size: 199 KiB |
Before Width: | Height: | Size: 506 KiB After Width: | Height: | Size: 427 KiB |
Before Width: | Height: | Size: 721 KiB |
Before Width: | Height: | Size: 456 KiB |
Before Width: | Height: | Size: 501 KiB |
Before Width: | Height: | Size: 599 KiB After Width: | Height: | Size: 537 KiB |
@ -30,18 +30,13 @@ Prerequisites:
|
||||
"ssm:DeleteParameter",
|
||||
"ssm:GetParametersByPath",
|
||||
"ssm:DeleteParameters",
|
||||
"ssm:AddTagsToResource", // if you need to add tags to secrets
|
||||
"kms:ListKeys", // if you need to specify the KMS key
|
||||
"kms:ListAliases", // if you need to specify the KMS key
|
||||
"kms:Encrypt", // if you need to specify the KMS key
|
||||
"kms:Decrypt" // if you need to specify the KMS key
|
||||
"ssm:AddTagsToResource" // if you need to add tags to secrets
|
||||
],
|
||||
"Resource": "*"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
</Step>
|
||||
<Step title="Authorize Infisical for AWS Parameter store">
|
||||
Obtain a AWS access key ID and secret access key for your IAM user in IAM > Users > User > Security credentials > Access keys
|
||||
@ -49,7 +44,7 @@ Prerequisites:
|
||||

|
||||

|
||||

|
||||
|
||||
|
||||
Navigate to your project's integrations tab in Infisical.
|
||||
|
||||

|
||||
@ -64,7 +59,6 @@ Prerequisites:
|
||||
breaks E2EE, it's necessary for Infisical to sync the environment variables to
|
||||
the cloud platform.
|
||||
</Info>
|
||||
|
||||
</Step>
|
||||
<Step title="Start integration">
|
||||
Select which Infisical environment secrets you want to sync to which AWS Parameter Store region and indicate the path for your secrets. Then, press create integration to start syncing secrets to AWS Parameter Store.
|
||||
@ -78,6 +72,6 @@ Prerequisites:
|
||||
secret like `TEST` to be stored as `/[project_name]/[environment]/TEST` in AWS
|
||||
Parameter Store.
|
||||
</Tip>
|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
|
@ -22,7 +22,7 @@ description: "Learn how to use Helm chart to install Infisical on your Kubernete
|
||||
</Step>
|
||||
<Step title="Select Infisical version">
|
||||
By default, the Infisical version set in your helm chart will likely be outdated.
|
||||
Choose the latest Infisical docker image tag from [here](https://hub.docker.com/r/infisical/infisical/tags).
|
||||
Choose the latest Infisical docker image tag from here [here](https://hub.docker.com/r/infisical/infisical/tags).
|
||||
|
||||
|
||||
```yaml values.yaml
|
||||
|
52
frontend/package-lock.json
generated
@ -38,7 +38,6 @@
|
||||
"@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",
|
||||
@ -5777,57 +5776,6 @@
|
||||
"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",
|
||||
|
@ -46,7 +46,6 @@
|
||||
"@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",
|
||||
|
Before Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 9.0 KiB |
Before Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 26 KiB |
@ -1,4 +1,28 @@
|
||||
[
|
||||
{
|
||||
"name": "Docker",
|
||||
"slug": "docker",
|
||||
"image": "Docker",
|
||||
"docsLink": "https://infisical.com/docs/integrations/platforms/docker"
|
||||
},
|
||||
{
|
||||
"name": "Docker Compose",
|
||||
"slug": "docker-compose",
|
||||
"image": "Docker Compose",
|
||||
"docsLink": "https://infisical.com/docs/integrations/platforms/docker-compose"
|
||||
},
|
||||
{
|
||||
"name": "Kubernetes",
|
||||
"slug": "kubernetes",
|
||||
"image": "Kubernetes",
|
||||
"docsLink": "https://infisical.com/docs/integrations/platforms/kubernetes"
|
||||
},
|
||||
{
|
||||
"name": "Terraform",
|
||||
"slug": "terraform",
|
||||
"image": "Terraform",
|
||||
"docsLink": "https://infisical.com/docs/integrations/frameworks/terraform"
|
||||
},
|
||||
{
|
||||
"name": "React",
|
||||
"slug": "react",
|
||||
|
@ -1,50 +0,0 @@
|
||||
[
|
||||
{
|
||||
"name": "Docker",
|
||||
"slug": "docker",
|
||||
"image": "Docker",
|
||||
"docsLink": "https://infisical.com/docs/integrations/platforms/docker"
|
||||
},
|
||||
{
|
||||
"name": "Docker Compose",
|
||||
"slug": "docker-compose",
|
||||
"image": "Docker Compose",
|
||||
"docsLink": "https://infisical.com/docs/integrations/platforms/docker-compose"
|
||||
},
|
||||
{
|
||||
"name": "Kubernetes",
|
||||
"slug": "kubernetes",
|
||||
"image": "Kubernetes",
|
||||
"docsLink": "https://infisical.com/docs/integrations/platforms/kubernetes"
|
||||
},
|
||||
{
|
||||
"name": "Terraform",
|
||||
"slug": "terraform",
|
||||
"image": "Terraform",
|
||||
"docsLink": "https://infisical.com/docs/integrations/frameworks/terraform"
|
||||
},
|
||||
{
|
||||
"name": "Jenkins",
|
||||
"slug": "jenkins",
|
||||
"image": "Jenkins",
|
||||
"docsLink": "https://infisical.com/docs/integrations/cicd/jenkins"
|
||||
},
|
||||
{
|
||||
"name": "Infisical Agent",
|
||||
"slug": "agent",
|
||||
"image": "Agent",
|
||||
"docsLink": "https://infisical.com/docs/integrations/platforms/infisical-agent"
|
||||
},
|
||||
{
|
||||
"name": "Amazon ECS",
|
||||
"slug": "ecs",
|
||||
"image": "ECS",
|
||||
"docsLink": "https://infisical.com/docs/integrations/platforms/ecs-with-agent"
|
||||
},
|
||||
{
|
||||
"name": "Ansible",
|
||||
"slug": "ansible",
|
||||
"image": "Ansible",
|
||||
"docsLink": "https://infisical.com/docs/integrations/platforms/ansible"
|
||||
}
|
||||
]
|
@ -120,7 +120,7 @@
|
||||
"available": "Platform & Cloud Integrations",
|
||||
"available-text1": "Click on the integration you want to connect. This will let your environment variables flow automatically into selected third-party services.",
|
||||
"available-text2": "Note: during an integration with Heroku, for security reasons, it is impossible to maintain end-to-end encryption. In theory, this lets Infisical decrypt yor environment variables. In practice, we can assure you that this will never be done, and it allows us to protect your secrets from bad actors online. The core Infisical service will always stay end-to-end encrypted. With any questions, reach out support@infisical.com.",
|
||||
"cloud-integrations": "Native Integrations",
|
||||
"cloud-integrations": "Cloud Integrations",
|
||||
"framework-integrations": "Framework Integrations",
|
||||
"click-to-start": "Click on an integration to begin syncing secrets to it.",
|
||||
"click-to-setup": "Click on a framework to get the setup instructions.",
|
||||
|
@ -1,375 +0,0 @@
|
||||
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 w-full 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";
|
@ -1 +0,0 @@
|
||||
export { InfisicalSecretInput } from "./InfisicalSecretInput";
|
@ -1,178 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
@ -1 +0,0 @@
|
||||
export { SecretPathInput } from "./SecretPathInput";
|
@ -1,7 +1 @@
|
||||
export {
|
||||
useCreateLDAPConfig,
|
||||
useCreateLDAPGroupMapping,
|
||||
useDeleteLDAPGroupMapping,
|
||||
useTestLDAPConnection,
|
||||
useUpdateLDAPConfig} from "./mutations";
|
||||
export { useGetLDAPConfig, useGetLDAPGroupMaps } from "./queries";
|
||||
export { useCreateLDAPConfig, useGetLDAPConfig, useUpdateLDAPConfig } from "./queries";
|
||||
|
@ -1,155 +0,0 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { ldapConfigKeys } from "./queries";
|
||||
|
||||
export const useCreateLDAPConfig = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
organizationId,
|
||||
isActive,
|
||||
url,
|
||||
bindDN,
|
||||
bindPass,
|
||||
searchBase,
|
||||
searchFilter,
|
||||
groupSearchBase,
|
||||
groupSearchFilter,
|
||||
caCert
|
||||
}: {
|
||||
organizationId: string;
|
||||
isActive: boolean;
|
||||
url: string;
|
||||
bindDN: string;
|
||||
bindPass: string;
|
||||
searchBase: string;
|
||||
searchFilter: string;
|
||||
groupSearchBase: string;
|
||||
groupSearchFilter: string;
|
||||
caCert?: string;
|
||||
}) => {
|
||||
const { data } = await apiRequest.post("/api/v1/ldap/config", {
|
||||
organizationId,
|
||||
isActive,
|
||||
url,
|
||||
bindDN,
|
||||
bindPass,
|
||||
searchBase,
|
||||
searchFilter,
|
||||
groupSearchBase,
|
||||
groupSearchFilter,
|
||||
caCert
|
||||
});
|
||||
|
||||
return data;
|
||||
},
|
||||
onSuccess(_, dto) {
|
||||
queryClient.invalidateQueries(ldapConfigKeys.getLDAPConfig(dto.organizationId));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateLDAPConfig = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
organizationId,
|
||||
isActive,
|
||||
url,
|
||||
bindDN,
|
||||
bindPass,
|
||||
searchBase,
|
||||
searchFilter,
|
||||
groupSearchBase,
|
||||
groupSearchFilter,
|
||||
caCert
|
||||
}: {
|
||||
organizationId: string;
|
||||
isActive?: boolean;
|
||||
url?: string;
|
||||
bindDN?: string;
|
||||
bindPass?: string;
|
||||
searchBase?: string;
|
||||
searchFilter?: string;
|
||||
groupSearchBase?: string;
|
||||
groupSearchFilter?: string;
|
||||
caCert?: string;
|
||||
}) => {
|
||||
const { data } = await apiRequest.patch("/api/v1/ldap/config", {
|
||||
organizationId,
|
||||
isActive,
|
||||
url,
|
||||
bindDN,
|
||||
bindPass,
|
||||
searchBase,
|
||||
searchFilter,
|
||||
groupSearchBase,
|
||||
groupSearchFilter,
|
||||
caCert
|
||||
});
|
||||
|
||||
return data;
|
||||
},
|
||||
onSuccess(_, dto) {
|
||||
queryClient.invalidateQueries(ldapConfigKeys.getLDAPConfig(dto.organizationId));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useCreateLDAPGroupMapping = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
ldapConfigId,
|
||||
ldapGroupCN,
|
||||
groupSlug
|
||||
}: {
|
||||
ldapConfigId: string;
|
||||
ldapGroupCN: string;
|
||||
groupSlug: string;
|
||||
}) => {
|
||||
const { data } = await apiRequest.post(`/api/v1/ldap/config/${ldapConfigId}/group-maps`, {
|
||||
ldapGroupCN,
|
||||
groupSlug
|
||||
});
|
||||
return data;
|
||||
},
|
||||
onSuccess(_, { ldapConfigId }) {
|
||||
queryClient.invalidateQueries(ldapConfigKeys.getLDAPGroupMaps(ldapConfigId));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteLDAPGroupMapping = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
ldapConfigId,
|
||||
ldapGroupMapId
|
||||
}: {
|
||||
ldapConfigId: string;
|
||||
ldapGroupMapId: string;
|
||||
}) => {
|
||||
const { data } = await apiRequest.delete(
|
||||
`/api/v1/ldap/config/${ldapConfigId}/group-maps/${ldapGroupMapId}`
|
||||
);
|
||||
return data;
|
||||
},
|
||||
onSuccess(_, { ldapConfigId }) {
|
||||
queryClient.invalidateQueries(ldapConfigKeys.getLDAPGroupMaps(ldapConfigId));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useTestLDAPConnection = () => {
|
||||
return useMutation({
|
||||
mutationFn: async (ldapConfigId: string) => {
|
||||
const { data } = await apiRequest.post<boolean>(
|
||||
`/api/v1/ldap/config/${ldapConfigId}/test-connection`
|
||||
);
|
||||
return data;
|
||||
}
|
||||
});
|
||||
};
|
@ -1,12 +1,9 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { LDAPGroupMap } from "./types";
|
||||
|
||||
export const ldapConfigKeys = {
|
||||
getLDAPConfig: (orgId: string) => [{ orgId }, "organization-ldap"] as const,
|
||||
getLDAPGroupMaps: (ldapConfigId: string) => [{ ldapConfigId }, "ldap-group-maps"] as const
|
||||
const ldapConfigKeys = {
|
||||
getLDAPConfig: (orgId: string) => [{ orgId }, "organization-ldap"] as const
|
||||
};
|
||||
|
||||
export const useGetLDAPConfig = (organizationId: string) => {
|
||||
@ -21,18 +18,78 @@ export const useGetLDAPConfig = (organizationId: string) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetLDAPGroupMaps = (ldapConfigId: string) => {
|
||||
return useQuery({
|
||||
queryKey: ldapConfigKeys.getLDAPGroupMaps(ldapConfigId),
|
||||
queryFn: async () => {
|
||||
if (!ldapConfigId) return [];
|
||||
|
||||
const { data } = await apiRequest.get<LDAPGroupMap[]>(
|
||||
`/api/v1/ldap/config/${ldapConfigId}/group-maps`
|
||||
);
|
||||
export const useCreateLDAPConfig = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
organizationId,
|
||||
isActive,
|
||||
url,
|
||||
bindDN,
|
||||
bindPass,
|
||||
searchBase,
|
||||
caCert
|
||||
}: {
|
||||
organizationId: string;
|
||||
isActive: boolean;
|
||||
url: string;
|
||||
bindDN: string;
|
||||
bindPass: string;
|
||||
searchBase: string;
|
||||
caCert?: string;
|
||||
}) => {
|
||||
const { data } = await apiRequest.post("/api/v1/ldap/config", {
|
||||
organizationId,
|
||||
isActive,
|
||||
url,
|
||||
bindDN,
|
||||
bindPass,
|
||||
searchBase,
|
||||
caCert
|
||||
});
|
||||
|
||||
return data;
|
||||
},
|
||||
enabled: true
|
||||
onSuccess(_, dto) {
|
||||
queryClient.invalidateQueries(ldapConfigKeys.getLDAPConfig(dto.organizationId));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateLDAPConfig = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
organizationId,
|
||||
isActive,
|
||||
url,
|
||||
bindDN,
|
||||
bindPass,
|
||||
searchBase,
|
||||
caCert
|
||||
}: {
|
||||
organizationId: string;
|
||||
isActive?: boolean;
|
||||
url?: string;
|
||||
bindDN?: string;
|
||||
bindPass?: string;
|
||||
searchBase?: string;
|
||||
caCert?: string;
|
||||
}) => {
|
||||
const { data } = await apiRequest.patch("/api/v1/ldap/config", {
|
||||
organizationId,
|
||||
isActive,
|
||||
url,
|
||||
bindDN,
|
||||
bindPass,
|
||||
searchBase,
|
||||
caCert
|
||||
});
|
||||
|
||||
return data;
|
||||
},
|
||||
onSuccess(_, dto) {
|
||||
queryClient.invalidateQueries(ldapConfigKeys.getLDAPConfig(dto.organizationId));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -1,10 +0,0 @@
|
||||
export type LDAPGroupMap = {
|
||||
id: string;
|
||||
ldapConfigId: string;
|
||||
ldapGroupCN: string;
|
||||
group: {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
};
|
||||
};
|
@ -1,16 +1,14 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Head from "next/head";
|
||||
import frameworkIntegrationOptions from "public/json/frameworkIntegrations.json";
|
||||
import infrastructureIntegrationOptions from "public/json/infrastructureIntegrations.json";
|
||||
|
||||
import { IntegrationsPage } from "@app/views/IntegrationsPage";
|
||||
|
||||
type Props = {
|
||||
frameworkIntegrations: typeof frameworkIntegrationOptions;
|
||||
infrastructureIntegrations: typeof infrastructureIntegrationOptions;
|
||||
};
|
||||
|
||||
const Integration = ({ frameworkIntegrations, infrastructureIntegrations }: Props) => {
|
||||
const Integration = ({ frameworkIntegrations }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
@ -22,7 +20,7 @@ const Integration = ({ frameworkIntegrations, infrastructureIntegrations }: Prop
|
||||
<meta property="og:title" content="Manage your .env files in seconds" />
|
||||
<meta name="og:description" content={t("integrations.description") as string} />
|
||||
</Head>
|
||||
<IntegrationsPage frameworkIntegrations={frameworkIntegrations} infrastructureIntegrations={infrastructureIntegrations} />
|
||||
<IntegrationsPage frameworkIntegrations={frameworkIntegrations} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -30,8 +28,7 @@ const Integration = ({ frameworkIntegrations, infrastructureIntegrations }: Prop
|
||||
export const getStaticProps = () => {
|
||||
return {
|
||||
props: {
|
||||
frameworkIntegrations: frameworkIntegrationOptions,
|
||||
infrastructureIntegrations: infrastructureIntegrationOptions
|
||||
frameworkIntegrations: frameworkIntegrationOptions
|
||||
}
|
||||
};
|
||||
};
|
||||
|
@ -14,7 +14,6 @@ import { motion } from "framer-motion";
|
||||
import queryString from "query-string";
|
||||
|
||||
import { useCreateIntegration } from "@app/hooks/api";
|
||||
import { useGetIntegrationAuthAwsKmsKeys } from "@app/hooks/api/integrationAuth/queries";
|
||||
|
||||
import {
|
||||
Button,
|
||||
@ -91,7 +90,6 @@ export default function AWSParameterStoreCreateIntegrationPage() {
|
||||
const [shouldTag, setShouldTag] = useState(false);
|
||||
const [tagKey, setTagKey] = useState("");
|
||||
const [tagValue, setTagValue] = useState("");
|
||||
const [kmsKeyId, setKmsKeyId] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (workspace) {
|
||||
@ -100,19 +98,6 @@ export default function AWSParameterStoreCreateIntegrationPage() {
|
||||
}
|
||||
}, [workspace]);
|
||||
|
||||
|
||||
const { data: integrationAuthAwsKmsKeys, isLoading: isIntegrationAuthAwsKmsKeysLoading } =
|
||||
useGetIntegrationAuthAwsKmsKeys({
|
||||
integrationAuthId: String(integrationAuthId),
|
||||
region: selectedAWSRegion
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (integrationAuthAwsKmsKeys) {
|
||||
setKmsKeyId(String(integrationAuthAwsKmsKeys?.filter(key => key.alias === "default")[0]?.id))
|
||||
}
|
||||
}, [integrationAuthAwsKmsKeys])
|
||||
|
||||
const isValidAWSParameterStorePath = (awsStorePath: string) => {
|
||||
const pattern = /^\/([\w-]+\/)*[\w-]+\/$/;
|
||||
return pattern.test(awsStorePath) && awsStorePath.length <= 2048;
|
||||
@ -148,11 +133,7 @@ export default function AWSParameterStoreCreateIntegrationPage() {
|
||||
value: tagValue
|
||||
}]
|
||||
}
|
||||
: {}),
|
||||
...((kmsKeyId && integrationAuthAwsKmsKeys?.filter(key => key.id === kmsKeyId)[0]?.alias !== "default") ?
|
||||
{
|
||||
kmsKeyId
|
||||
}: {})
|
||||
: {})
|
||||
}
|
||||
});
|
||||
|
||||
@ -165,7 +146,7 @@ export default function AWSParameterStoreCreateIntegrationPage() {
|
||||
}
|
||||
};
|
||||
|
||||
return (integrationAuth && workspace && selectedSourceEnvironment && !isIntegrationAuthAwsKmsKeysLoading) ? (
|
||||
return integrationAuth && workspace && selectedSourceEnvironment ? (
|
||||
<div className="flex h-full w-full flex-col items-center justify-center">
|
||||
<Head>
|
||||
<title>Set Up AWS Parameter Integration</title>
|
||||
@ -305,31 +286,6 @@ export default function AWSParameterStoreCreateIntegrationPage() {
|
||||
</FormControl>
|
||||
</div>
|
||||
)}
|
||||
<FormControl label="Encryption Key" className="mt-4">
|
||||
<Select
|
||||
value={kmsKeyId}
|
||||
onValueChange={(e) => {
|
||||
setKmsKeyId(e)
|
||||
}}
|
||||
className="w-full border border-mineshaft-500"
|
||||
>
|
||||
{integrationAuthAwsKmsKeys?.length ? (
|
||||
integrationAuthAwsKmsKeys.map((key) => {
|
||||
return (
|
||||
<SelectItem
|
||||
value={key.id as string}
|
||||
key={`repo-id-${key.id}`}
|
||||
className="w-[28.4rem] text-sm"
|
||||
>
|
||||
{key.alias}
|
||||
</SelectItem>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</motion.div>
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
@ -362,7 +318,7 @@ export default function AWSParameterStoreCreateIntegrationPage() {
|
||||
<title>Set Up AWS Parameter Store Integration</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
</Head>
|
||||
{(isintegrationAuthLoading || isIntegrationAuthAwsKmsKeysLoading) ? (
|
||||
{isintegrationAuthLoading ? (
|
||||
<img
|
||||
src="/images/loading/loading.gif"
|
||||
height={70}
|
||||
|
@ -148,7 +148,7 @@ export default function AWSSecretManagerCreateIntegrationPage() {
|
||||
}]
|
||||
}
|
||||
: {}),
|
||||
...((kmsKeyId && integrationAuthAwsKmsKeys?.filter(key => key.id === kmsKeyId)[0]?.alias !== "default") ?
|
||||
...((kmsKeyId && integrationAuthAwsKmsKeys?.filter(key => key.id === kmsKeyId)[0]?.alias !== "alias/aws/secretsmanager") ?
|
||||
{
|
||||
kmsKeyId
|
||||
}: {})
|
||||
|
@ -4,10 +4,16 @@ 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, Select, SelectItem } from "../../../components/v2";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CardTitle,
|
||||
FormControl,
|
||||
Input,
|
||||
Select,
|
||||
SelectItem} from "../../../components/v2";
|
||||
import {
|
||||
useGetIntegrationAuthApps,
|
||||
useGetIntegrationAuthById
|
||||
@ -21,6 +27,7 @@ const cloudflareEnvironments = [
|
||||
export default function CloudflarePagesIntegrationPage() {
|
||||
const router = useRouter();
|
||||
const { mutateAsync } = useCreateIntegration();
|
||||
|
||||
|
||||
const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]);
|
||||
const [secretPath, setSecretPath] = useState("/");
|
||||
@ -123,10 +130,9 @@ export default function CloudflarePagesIntegrationPage() {
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControl label="Infisical Secret Path" className="mt-2 px-6">
|
||||
<SecretPathInput
|
||||
<Input
|
||||
value={secretPath}
|
||||
onChange={(value) => setSecretPath(value)}
|
||||
environment={selectedSourceEnvironment}
|
||||
onChange={(evt) => setSecretPath(evt.target.value)}
|
||||
placeholder="Provide a path, default is /"
|
||||
/>
|
||||
</FormControl>
|
||||
|
@ -4,10 +4,17 @@ 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, Select, SelectItem } from "../../../components/v2";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CardTitle,
|
||||
FormControl,
|
||||
Input,
|
||||
Select,
|
||||
SelectItem
|
||||
} from "../../../components/v2";
|
||||
import {
|
||||
useGetIntegrationAuthApps,
|
||||
useGetIntegrationAuthById
|
||||
@ -16,6 +23,7 @@ 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") ?? "");
|
||||
@ -114,10 +122,9 @@ export default function CloudflareWorkersIntegrationPage() {
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControl label="Infisical Secret Path" className="mt-2 px-6">
|
||||
<SecretPathInput
|
||||
<Input
|
||||
value={secretPath}
|
||||
onChange={(value) => setSecretPath(value)}
|
||||
environment={selectedSourceEnvironment}
|
||||
onChange={(evt) => setSecretPath(evt.target.value)}
|
||||
placeholder="Provide a path, default is /"
|
||||
/>
|
||||
</FormControl>
|
||||
|
@ -11,7 +11,6 @@ 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";
|
||||
|
||||
@ -259,11 +258,7 @@ export default function GCPSecretManagerCreateIntegrationPage() {
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<SecretPathInput
|
||||
{...field}
|
||||
environment={selectedSourceEnvironment}
|
||||
placeholder="/"
|
||||
/>
|
||||
<Input {...field} placeholder="/" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
@ -11,7 +11,6 @@ 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";
|
||||
|
||||
@ -269,11 +268,7 @@ export default function GitLabCreateIntegrationPage() {
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<SecretPathInput
|
||||
{...field}
|
||||
placeholder="/"
|
||||
environment={selectedSourceEnvironment}
|
||||
/>
|
||||
<Input {...field} placeholder="/" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
@ -9,8 +9,15 @@ import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import queryString from "query-string";
|
||||
import * as yup from "yup";
|
||||
|
||||
import { Button, Card, CardTitle, FormControl, Select, SelectItem } from "@app/components/v2";
|
||||
import { SecretPathInput } from "@app/components/v2/SecretPathInput";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CardTitle,
|
||||
FormControl,
|
||||
Input,
|
||||
Select,
|
||||
SelectItem
|
||||
} from "@app/components/v2";
|
||||
import { useCreateIntegration } from "@app/hooks/api";
|
||||
import {
|
||||
useGetIntegrationAuthApps,
|
||||
@ -31,7 +38,6 @@ export default function HasuraCloudCreateIntegrationPage() {
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
watch,
|
||||
formState: { isSubmitting }
|
||||
} = useForm<FormData>({
|
||||
resolver: yupResolver(schema)
|
||||
@ -45,8 +51,6 @@ export default function HasuraCloudCreateIntegrationPage() {
|
||||
(integrationAuthId as string) ?? ""
|
||||
);
|
||||
|
||||
const selectedSourceEnvironment = watch("sourceEnvironment");
|
||||
|
||||
const { data: integrationAuthApps, isLoading: isIntegrationAuthAppsLoading } =
|
||||
useGetIntegrationAuthApps({
|
||||
integrationAuthId: (integrationAuthId as string) ?? ""
|
||||
@ -143,7 +147,7 @@ export default function HasuraCloudCreateIntegrationPage() {
|
||||
name="secretPath"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl label="Secrets Path" errorText={error?.message} isError={Boolean(error)}>
|
||||
<SecretPathInput {...field} environment={selectedSourceEnvironment} />
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
@ -17,12 +17,19 @@ 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, Select, SelectItem } from "../../../components/v2";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CardTitle,
|
||||
FormControl,
|
||||
Input,
|
||||
Select,
|
||||
SelectItem
|
||||
} from "../../../components/v2";
|
||||
import {
|
||||
useGetIntegrationAuthApps,
|
||||
useGetIntegrationAuthById
|
||||
@ -273,11 +280,7 @@ export default function HerokuCreateIntegrationPage() {
|
||||
name="secretPath"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl label="Secrets Path" isError={Boolean(error)} errorText={error?.message}>
|
||||
<SecretPathInput
|
||||
{...field}
|
||||
placeholder="/"
|
||||
environment={selectedSourceEnvironment}
|
||||
/>
|
||||
<Input {...field} placeholder="/" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
@ -15,7 +15,6 @@ 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 {
|
||||
@ -23,6 +22,7 @@ import {
|
||||
Card,
|
||||
CardTitle,
|
||||
FormControl,
|
||||
Input,
|
||||
Select,
|
||||
SelectItem,
|
||||
Switch
|
||||
@ -185,11 +185,7 @@ export default function RenderCreateIntegrationPage() {
|
||||
name="secretPath"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl label="Secrets Path" isError={Boolean(error)} errorText={error?.message}>
|
||||
<SecretPathInput
|
||||
{...field}
|
||||
placeholder="/"
|
||||
environment={selectedSourceEnvironment}
|
||||
/>
|
||||
<Input {...field} placeholder="/" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
@ -594,7 +594,7 @@ const OrganizationPage = withPermission(
|
||||
</div>
|
||||
)}
|
||||
<div className="mb-4 flex flex-col items-start justify-start px-6 py-6 pb-0 text-3xl">
|
||||
{window.location.origin.includes("https://app.infisical.com") || window.location.origin.includes("http://localhost:8080") && (
|
||||
{(window.location.origin.includes("https://app.infisical.com") || window.location.origin.includes("http://localhost:8080")) && (
|
||||
<div
|
||||
className={`${
|
||||
!updateClosed ? "block" : "hidden"
|
||||
@ -602,10 +602,10 @@ const OrganizationPage = withPermission(
|
||||
>
|
||||
<FontAwesomeIcon icon={faWarning} className="p-6 text-4xl text-primary" />
|
||||
<div className="text-sm">
|
||||
<span className="text-lg font-semibold">Scheduled maintenance on April 13th 2024 </span>{" "}
|
||||
<span className="text-lg font-semibold">Scheduled maintenance on May 11th 2024 </span>{" "}
|
||||
<br />
|
||||
Infisical will undergo scheduled maintenance for approximately 1 hour on Saturday, April 13th, 11am EST. During these hours, read
|
||||
operations will continue to function normally but no resources will be editable.
|
||||
Infisical will undergo scheduled maintenance for approximately 2 hour on Saturday, May 11th, 11am EST. During these hours, read
|
||||
operations to Infisical will continue to function normally but no resources will be editable.
|
||||
No action is required on your end — your applications will continue to fetch secrets.
|
||||
<br />
|
||||
</div>
|
||||
|
@ -21,17 +21,15 @@ import { ProjectVersion } from "@app/hooks/api/workspace/types";
|
||||
|
||||
import { CloudIntegrationSection } from "./components/CloudIntegrationSection";
|
||||
import { FrameworkIntegrationSection } from "./components/FrameworkIntegrationSection";
|
||||
import { InfrastructureIntegrationSection } from "./components/InfrastructureIntegrationSection/InfrastructureIntegrationSection";
|
||||
import { IntegrationsSection } from "./components/IntegrationsSection";
|
||||
import { generateBotKey, redirectForProviderAuth } from "./IntegrationPage.utils";
|
||||
|
||||
type Props = {
|
||||
frameworkIntegrations: Array<{ name: string; slug: string; image: string; docsLink: string }>;
|
||||
infrastructureIntegrations: Array<{ name: string; slug: string; image: string; docsLink: string }>;
|
||||
};
|
||||
|
||||
export const IntegrationsPage = withProjectPermission(
|
||||
({ frameworkIntegrations, infrastructureIntegrations }: Props) => {
|
||||
({ frameworkIntegrations }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
|
||||
@ -230,7 +228,6 @@ export const IntegrationsPage = withProjectPermission(
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
<FrameworkIntegrationSection frameworks={frameworkIntegrations} />
|
||||
<InfrastructureIntegrationSection integrations={infrastructureIntegrations} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
@ -109,7 +109,7 @@ export const CloudIntegrationSection = ({
|
||||
</div>
|
||||
{cloudIntegration.isAvailable &&
|
||||
Boolean(integrationAuths?.[cloudIntegration.slug]) && (
|
||||
<div className="absolute top-0 right-0 z-30 h-full">
|
||||
<div className="absolute top-0 right-0 z-40 h-full">
|
||||
<div className="relative h-full">
|
||||
<div className="absolute top-0 right-0 w-24 flex-row items-center overflow-hidden whitespace-nowrap rounded-tr-md rounded-bl-md bg-primary py-0.5 px-2 text-xs text-black opacity-80 transition-all duration-300 group-hover:w-0 group-hover:p-0">
|
||||
<FontAwesomeIcon icon={faCheck} className="mr-2 text-xs" />
|
||||
|
@ -1,7 +1,4 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { faKeyboard } from "@fortawesome/free-regular-svg-icons";
|
||||
import { faComputer } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
type Props = {
|
||||
frameworks: Array<{
|
||||
@ -53,36 +50,6 @@ export const FrameworkIntegrationSection = ({ frameworks }: Props) => {
|
||||
</div>
|
||||
</a>
|
||||
))}
|
||||
<a
|
||||
key="framework-integration-more"
|
||||
href="https://infisical.com/docs/cli/commands/run"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
className="relative flex h-32 cursor-pointer flex-row items-center justify-center rounded-md p-0.5 duration-200"
|
||||
>
|
||||
<div
|
||||
className="flex h-full w-full cursor-pointer flex-col items-center justify-center rounded-md border border-mineshaft-600 bg-mineshaft-800 font-semibold text-gray-300 duration-200 hover:bg-mineshaft-700 group-hover:text-gray-200 px-1 text-xl w-full max-w-xs text-center"
|
||||
>
|
||||
<FontAwesomeIcon className="text-5xl mb-2 text-white/90" icon={faKeyboard} />
|
||||
<div className="h-2" />
|
||||
CLI
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
key="framework-integration-more"
|
||||
href="https://infisical.com/docs/sdks/overview"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
className="relative flex h-32 cursor-pointer flex-row items-center justify-center rounded-md p-0.5 duration-200"
|
||||
>
|
||||
<div
|
||||
className="flex h-full w-full cursor-pointer flex-col items-center justify-center rounded-md border border-mineshaft-600 bg-mineshaft-800 font-semibold text-gray-300 duration-200 hover:bg-mineshaft-700 group-hover:text-gray-200 px-1 text-xl w-full max-w-xs text-center"
|
||||
>
|
||||
<FontAwesomeIcon className="text-5xl mb-1 text-white/90" icon={faComputer} />
|
||||
<div className="h-2" />
|
||||
SDKs
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
@ -1,50 +0,0 @@
|
||||
type Props = {
|
||||
integrations: Array<{
|
||||
name: string;
|
||||
image: string;
|
||||
slug: string;
|
||||
docsLink: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
export const InfrastructureIntegrationSection = ({ integrations }: Props) => {
|
||||
const sortedIntegrations = integrations.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mx-4 mt-12 mb-4 flex flex-col items-start justify-between px-2 text-xl">
|
||||
<h1 className="text-3xl font-semibold">Infrastructure Integrations</h1>
|
||||
<p className="text-base text-gray-400">Click on of the integration to read the documentation.</p>
|
||||
</div>
|
||||
<div className="mx-6 grid grid-cols-2 gap-4 lg:grid-cols-3 2xl:grid-cols-4">
|
||||
{sortedIntegrations.map((integration) => (
|
||||
<a
|
||||
key={`framework-integration-${integration.slug}`}
|
||||
href={integration.docsLink}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
className="relative w-full flex h-32 cursor-pointer flex-row items-center justify-center rounded-md p-0.5 duration-200"
|
||||
>
|
||||
<div
|
||||
onKeyDown={() => null}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className="group relative w-full cursor-pointer duration-200 hover:bg-mineshaft-700 flex h-32 flex-row items-center rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4"
|
||||
key={integration?.name}
|
||||
>
|
||||
<img
|
||||
src={`/images/integrations/${integration.image}.png`}
|
||||
height={integration?.name ? 60 : 90}
|
||||
width={integration?.name ? 60 : 90}
|
||||
alt="integration logo"
|
||||
/>
|
||||
<div className="ml-4 max-w-xs text-xl font-semibold text-gray-300 duration-200 group-hover:text-gray-200">
|
||||
{integration?.name && integration.name}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
@ -1 +0,0 @@
|
||||
export { InfrastructureIntegrationSection } from "./InfrastructureIntegrationSection";
|
@ -137,7 +137,7 @@ export const IntegrationsSection = ({
|
||||
"App"
|
||||
}
|
||||
/>
|
||||
<div className="min-w-[8rem] max-w-[12rem] overflow-scroll no-scrollbar no-scrollbar::-webkit-scrollbar whitespace-nowrap rounded-md border border-mineshaft-700 bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200">
|
||||
<div className="min-w-[8rem] max-w-[12rem] overflow-clip text-ellipsis whitespace-nowrap rounded-md border border-mineshaft-700 bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200">
|
||||
{(integration.integration === "hashicorp-vault" &&
|
||||
`${integration.app} - path: ${integration.path}`) ||
|
||||
(integration.scope === "github-org" && `${integration.owner}`) ||
|
||||
@ -217,12 +217,11 @@ export const IntegrationsSection = ({
|
||||
isOpen={popUp.deleteConfirmation.isOpen}
|
||||
title={`Are you sure want to remove ${
|
||||
(popUp?.deleteConfirmation.data as TIntegration)?.integration || " "
|
||||
} integration for ${(popUp?.deleteConfirmation.data as TIntegration)?.app || "this project"}?`}
|
||||
} integration for ${(popUp?.deleteConfirmation.data as TIntegration)?.app || " "}?`}
|
||||
onChange={(isOpen) => handlePopUpToggle("deleteConfirmation", isOpen)}
|
||||
deleteKey={
|
||||
(popUp?.deleteConfirmation?.data as TIntegration)?.app ||
|
||||
(popUp?.deleteConfirmation?.data as TIntegration)?.owner ||
|
||||
(popUp?.deleteConfirmation?.data as TIntegration)?.path ||
|
||||
""
|
||||
}
|
||||
onDeleteApproved={async () =>
|
||||
|
@ -34,7 +34,6 @@ import {
|
||||
Tag,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import { SecretPathInput } from "@app/components/v2/SecretPathInput";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub,
|
||||
@ -116,7 +115,6 @@ const SpecificPrivilegeSecretForm = ({
|
||||
});
|
||||
|
||||
const temporaryAccessField = privilegeForm.watch("temporaryAccess");
|
||||
const selectedEnvironmentSlug = privilegeForm.watch("environmentSlug");
|
||||
const isTemporary = temporaryAccessField?.isTemporary;
|
||||
const isExpired =
|
||||
temporaryAccessField.isTemporary &&
|
||||
@ -222,12 +220,7 @@ const SpecificPrivilegeSecretForm = ({
|
||||
name="secretPath"
|
||||
render={({ field }) => (
|
||||
<FormControl label="Secret Path">
|
||||
<SecretPathInput
|
||||
{...field}
|
||||
isDisabled={isMemberEditDisabled}
|
||||
environment={selectedEnvironmentSlug}
|
||||
containerClassName="w-48"
|
||||
/>
|
||||
<Input {...field} isDisabled={isMemberEditDisabled} className="w-48" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
@ -34,7 +34,6 @@ import {
|
||||
Tag,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import { SecretPathInput } from "@app/components/v2/SecretPathInput";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub,
|
||||
@ -108,7 +107,6 @@ const SpecificPrivilegeSecretForm = ({ privilege }: { privilege: TProjectUserPri
|
||||
});
|
||||
|
||||
const temporaryAccessField = privilegeForm.watch("temporaryAccess");
|
||||
const selectedEnvironmentSlug = privilegeForm.watch("environmentSlug");
|
||||
const isTemporary = temporaryAccessField?.isTemporary;
|
||||
const isExpired =
|
||||
temporaryAccessField.isTemporary &&
|
||||
@ -210,12 +208,7 @@ const SpecificPrivilegeSecretForm = ({ privilege }: { privilege: TProjectUserPri
|
||||
name="secretPath"
|
||||
render={({ field }) => (
|
||||
<FormControl label="Secret Path">
|
||||
<SecretPathInput
|
||||
{...field}
|
||||
isDisabled={isMemberEditDisabled}
|
||||
containerClassName="w-48"
|
||||
environment={selectedEnvironmentSlug}
|
||||
/>
|
||||
<Input {...field} isDisabled={isMemberEditDisabled} className="w-48" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
@ -20,7 +20,6 @@ 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";
|
||||
@ -60,14 +59,13 @@ 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(() => {
|
||||
@ -176,11 +174,7 @@ export const SecretPolicyForm = ({
|
||||
name="secretPath"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl label="Secret Path" isError={Boolean(error)} errorText={error?.message}>
|
||||
<SecretPathInput
|
||||
{...field}
|
||||
value={field.value || ""}
|
||||
environment={selectedEnvironment}
|
||||
/>
|
||||
<Input {...field} value={field.value || ""} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
@ -4,8 +4,15 @@ import { AxiosError } from "axios";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, FormControl, Modal, ModalContent, Select, SelectItem } from "@app/components/v2";
|
||||
import { SecretPathInput } from "@app/components/v2/SecretPathInput";
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
Input,
|
||||
Modal,
|
||||
ModalContent,
|
||||
Select,
|
||||
SelectItem
|
||||
} from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { useCreateSecretImport } from "@app/hooks/api";
|
||||
|
||||
@ -43,12 +50,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();
|
||||
|
||||
@ -123,7 +130,7 @@ export const CreateSecretImportForm = ({
|
||||
defaultValue="/"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl label="Secret Path" isError={Boolean(error)} errorText={error?.message}>
|
||||
<SecretPathInput {...field} environment={selectedEnvironment} />
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
@ -3,8 +3,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, FormControl, Input, Modal, ModalContent } from "@app/components/v2";
|
||||
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
|
||||
import { Button, FormControl, Input, Modal, ModalContent, SecretInput } from "@app/components/v2";
|
||||
import { useCreateSecretV3 } from "@app/hooks/api";
|
||||
import { UserWsKeyPair } from "@app/hooks/api/types";
|
||||
|
||||
@ -45,6 +44,8 @@ export const CreateSecretForm = ({
|
||||
const { isOpen } = usePopUpState(PopUpNames.CreateSecretForm);
|
||||
const { closePopUp, togglePopUp } = usePopUpAction();
|
||||
|
||||
|
||||
|
||||
const { mutateAsync: createSecretV3 } = useCreateSecretV3();
|
||||
|
||||
const handleFormSubmit = async ({ key, value }: TFormSchema) => {
|
||||
@ -102,10 +103,8 @@ export const CreateSecretForm = ({
|
||||
isError={Boolean(errors?.value)}
|
||||
errorText={errors?.value?.message}
|
||||
>
|
||||
<InfisicalSecretInput
|
||||
<SecretInput
|
||||
{...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>
|
||||
|
@ -28,7 +28,6 @@ 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";
|
||||
@ -77,6 +76,7 @@ export const CopySecretsFromBoard = ({
|
||||
handleSubmit,
|
||||
control,
|
||||
watch,
|
||||
register,
|
||||
reset,
|
||||
setValue,
|
||||
formState: { isDirty }
|
||||
@ -192,19 +192,9 @@ export const CopySecretsFromBoard = ({
|
||||
</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>
|
||||
)}
|
||||
/>
|
||||
<FormControl label="Secret Path" className="flex-grow" isRequired>
|
||||
<Input {...register("secretPath")} placeholder="Provide a path, default is /" />
|
||||
</FormControl>
|
||||
</div>
|
||||
<div className="border-t border-mineshaft-600 pt-4">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
|
@ -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,6 +71,7 @@ export const SecretDetailSidebar = ({
|
||||
environment,
|
||||
secretPath
|
||||
}: Props) => {
|
||||
|
||||
const {
|
||||
register,
|
||||
control,
|
||||
@ -203,10 +204,8 @@ export const SecretDetailSidebar = ({
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<FormControl label="Value">
|
||||
<InfisicalSecretInput
|
||||
<SecretInput
|
||||
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"
|
||||
@ -241,10 +240,8 @@ export const SecretDetailSidebar = ({
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<FormControl label="Value Override">
|
||||
<InfisicalSecretInput
|
||||
<SecretInput
|
||||
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}
|
||||
/>
|
||||
|
@ -14,6 +14,7 @@ import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
SecretInput,
|
||||
Spinner,
|
||||
TextArea,
|
||||
Tooltip
|
||||
@ -48,7 +49,6 @@ 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,12 +263,10 @@ export const SecretItem = memo(
|
||||
key="value-overriden"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<InfisicalSecretInput
|
||||
<SecretInput
|
||||
key="value-overriden"
|
||||
isVisible={isVisible}
|
||||
isReadOnly={isReadOnly}
|
||||
environment={environment}
|
||||
secretPath={secretPath}
|
||||
{...field}
|
||||
containerClassName="py-1.5 rounded-md transition-all group-hover:mr-2"
|
||||
/>
|
||||
@ -280,12 +278,10 @@ export const SecretItem = memo(
|
||||
key="secret-value"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<InfisicalSecretInput
|
||||
<SecretInput
|
||||
isReadOnly={isReadOnly}
|
||||
key="secret-value"
|
||||
isVisible={isVisible}
|
||||
environment={environment}
|
||||
secretPath={secretPath}
|
||||
{...field}
|
||||
containerClassName="py-1.5 rounded-md transition-all group-hover:mr-2"
|
||||
/>
|
||||
|
@ -120,9 +120,7 @@ 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>
|
||||
|
@ -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,6 +64,8 @@ export const CreateSecretForm = ({
|
||||
const workspaceId = currentWorkspace?.id || "";
|
||||
const environments = currentWorkspace?.environments || [];
|
||||
|
||||
|
||||
|
||||
const { mutateAsync: createSecretV3 } = useCreateSecretV3();
|
||||
const { mutateAsync: updateSecretV3 } = useUpdateSecretV3();
|
||||
const { mutateAsync: createFolder } = useCreateFolder();
|
||||
@ -161,7 +163,7 @@ export const CreateSecretForm = ({
|
||||
isError={Boolean(errors?.value)}
|
||||
errorText={errors?.value?.message}
|
||||
>
|
||||
<InfisicalSecretInput
|
||||
<SecretInput
|
||||
{...field}
|
||||
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-mineshaft-900 px-2 py-1.5"
|
||||
/>
|
||||
|
@ -6,8 +6,7 @@ import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { IconButton, Tooltip } from "@app/components/v2";
|
||||
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
|
||||
import { IconButton, SecretInput, Tooltip } from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||
import { useToggle } from "@app/hooks";
|
||||
|
||||
@ -50,6 +49,7 @@ export const SecretEditRow = ({
|
||||
}
|
||||
});
|
||||
const [isDeleting, setIsDeleting] = useToggle();
|
||||
|
||||
|
||||
const handleFormReset = () => {
|
||||
reset();
|
||||
@ -97,13 +97,10 @@ export const SecretEditRow = ({
|
||||
control={control}
|
||||
name="value"
|
||||
render={({ field }) => (
|
||||
<InfisicalSecretInput
|
||||
<SecretInput
|
||||
{...field}
|
||||
value={field.value as string}
|
||||
key="secret-input"
|
||||
isVisible={isVisible}
|
||||
secretPath={secretPath}
|
||||
environment={environment}
|
||||
isImport={isImportedSecret}
|
||||
/>
|
||||
)}
|
||||
|
@ -3,7 +3,6 @@ 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";
|
||||
|
||||
@ -79,7 +78,7 @@ export const RotationOutputForm = ({ onSubmit, onCancel, outputSchema = {} }: Pr
|
||||
defaultValue="/"
|
||||
render={({ field }) => (
|
||||
<FormControl className="capitalize" label="Secret path">
|
||||
<SecretPathInput {...field} environment={environment} />
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
@ -1,256 +0,0 @@
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { faUsers, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Button,
|
||||
DeleteActionModal,
|
||||
EmptyState,
|
||||
FormControl,
|
||||
IconButton,
|
||||
Input,
|
||||
Modal,
|
||||
ModalContent,
|
||||
Select,
|
||||
SelectItem,
|
||||
Table,
|
||||
TableContainer,
|
||||
TableSkeleton,
|
||||
TBody,
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { useOrganization } from "@app/context";
|
||||
import {
|
||||
useCreateLDAPGroupMapping,
|
||||
useDeleteLDAPGroupMapping,
|
||||
useGetLDAPConfig,
|
||||
useGetLDAPGroupMaps,
|
||||
useGetOrganizationGroups
|
||||
} from "@app/hooks/api";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
const schema = z.object({
|
||||
ldapGroupCN: z.string().min(1, "LDAP Group CN is required"),
|
||||
groupSlug: z.string().min(1, "Group Slug is required")
|
||||
});
|
||||
|
||||
export type TFormData = z.infer<typeof schema>;
|
||||
|
||||
type Props = {
|
||||
popUp: UsePopUpState<["ldapGroupMap", "deleteLdapGroupMap"]>;
|
||||
handlePopUpOpen: (
|
||||
popUpName: keyof UsePopUpState<["deleteLdapGroupMap"]>,
|
||||
data?: {
|
||||
ldapGroupMapId: string;
|
||||
ldapGroupCN: string;
|
||||
}
|
||||
) => void;
|
||||
handlePopUpToggle: (
|
||||
popUpName: keyof UsePopUpState<["ldapGroupMap", "deleteLdapGroupMap"]>,
|
||||
state?: boolean
|
||||
) => void;
|
||||
};
|
||||
|
||||
export const LDAPGroupMapModal = ({ popUp, handlePopUpOpen, handlePopUpToggle }: Props) => {
|
||||
const { currentOrg } = useOrganization();
|
||||
|
||||
const { data: ldapConfig } = useGetLDAPConfig(currentOrg?.id ?? "");
|
||||
const { data: groups } = useGetOrganizationGroups(currentOrg?.id ?? "");
|
||||
const { data: groupMaps, isLoading } = useGetLDAPGroupMaps(ldapConfig?.id ?? "");
|
||||
const { mutateAsync: createLDAPGroupMapping, isLoading: createIsLoading } =
|
||||
useCreateLDAPGroupMapping();
|
||||
const { mutateAsync: deleteLDAPGroupMapping } = useDeleteLDAPGroupMapping();
|
||||
|
||||
const { control, handleSubmit, reset } = useForm<TFormData>({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: {
|
||||
ldapGroupCN: "",
|
||||
groupSlug: ""
|
||||
}
|
||||
});
|
||||
|
||||
const onFormSubmit = async ({ groupSlug, ldapGroupCN }: TFormData) => {
|
||||
try {
|
||||
if (!ldapConfig) return;
|
||||
|
||||
await createLDAPGroupMapping({
|
||||
ldapConfigId: ldapConfig.id,
|
||||
groupSlug,
|
||||
ldapGroupCN
|
||||
});
|
||||
|
||||
reset();
|
||||
|
||||
createNotification({
|
||||
text: `Successfully added LDAP group mapping for ${ldapGroupCN}`,
|
||||
type: "success"
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: `Failed to add LDAP group mapping for ${ldapGroupCN}`,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onDeleteGroupMapSubmit = async ({
|
||||
ldapConfigId,
|
||||
ldapGroupMapId,
|
||||
ldapGroupCN
|
||||
}: {
|
||||
ldapConfigId: string;
|
||||
ldapGroupMapId: string;
|
||||
ldapGroupCN: string;
|
||||
}) => {
|
||||
try {
|
||||
await deleteLDAPGroupMapping({
|
||||
ldapConfigId,
|
||||
ldapGroupMapId
|
||||
});
|
||||
|
||||
handlePopUpToggle("deleteLdapGroupMap", false);
|
||||
|
||||
createNotification({
|
||||
text: `Successfully deleted LDAP group mapping ${ldapGroupCN}`,
|
||||
type: "success"
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: `Failed to delete LDAP group mapping ${ldapGroupCN}`,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={popUp?.ldapGroupMap?.isOpen}
|
||||
onOpenChange={(isOpen) => {
|
||||
handlePopUpToggle("ldapGroupMap", isOpen);
|
||||
reset();
|
||||
}}
|
||||
>
|
||||
<ModalContent title="Manage LDAP Group Mappings">
|
||||
<h2 className="mb-4">New Group Mapping</h2>
|
||||
<form onSubmit={handleSubmit(onFormSubmit)} className="mb-8">
|
||||
<div className="flex">
|
||||
<Controller
|
||||
control={control}
|
||||
name="ldapGroupCN"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="LDAP Group CN"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
<Input {...field} placeholder="Engineering" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="groupSlug"
|
||||
defaultValue=""
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Infisical Group"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
className="ml-4 w-full"
|
||||
>
|
||||
<div className="flex">
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
>
|
||||
{(groups || []).map(({ name, id, slug }) => (
|
||||
<SelectItem value={slug} key={`internal-group-${id}`}>
|
||||
{name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
<Button className="ml-4" size="sm" type="submit" isLoading={createIsLoading}>
|
||||
Add mapping
|
||||
</Button>
|
||||
</div>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
<h2 className="mb-4">Group Mappings</h2>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>LDAP Group CN</Th>
|
||||
<Th>Infisical Group</Th>
|
||||
<Th className="w-5" />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{isLoading && <TableSkeleton columns={3} innerKey="ldap-group-maps" />}
|
||||
{!isLoading &&
|
||||
groupMaps?.map(({ id, ldapGroupCN, group }) => {
|
||||
return (
|
||||
<Tr className="h-10 items-center" key={`ldap-group-map-${id}`}>
|
||||
<Td>{ldapGroupCN}</Td>
|
||||
<Td>{group.name}</Td>
|
||||
<Td>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
handlePopUpOpen("deleteLdapGroupMap", {
|
||||
ldapGroupMapId: id,
|
||||
ldapGroupCN
|
||||
});
|
||||
}}
|
||||
size="lg"
|
||||
colorSchema="danger"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
</TBody>
|
||||
</Table>
|
||||
{groupMaps?.length === 0 && (
|
||||
<EmptyState title="No LDAP group mappings found" icon={faUsers} />
|
||||
)}
|
||||
</TableContainer>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deleteLdapGroupMap.isOpen}
|
||||
title={`Are you sure want to delete the group mapping for ${
|
||||
(popUp?.deleteLdapGroupMap?.data as { ldapGroupCN: string })?.ldapGroupCN || ""
|
||||
}?`}
|
||||
onChange={(isOpen) => handlePopUpToggle("deleteLdapGroupMap", isOpen)}
|
||||
deleteKey="confirm"
|
||||
onDeleteApproved={() => {
|
||||
const deleteLdapGroupMapData = popUp?.deleteLdapGroupMap?.data as {
|
||||
ldapGroupMapId: string;
|
||||
ldapGroupCN: string;
|
||||
};
|
||||
return onDeleteGroupMapSubmit({
|
||||
ldapConfigId: ldapConfig?.id ?? "",
|
||||
ldapGroupMapId: deleteLdapGroupMapData.ldapGroupMapId,
|
||||
ldapGroupCN: deleteLdapGroupMapData.ldapGroupCN
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
@ -6,12 +6,7 @@ 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,
|
||||
useTestLDAPConnection,
|
||||
useUpdateLDAPConfig
|
||||
} from "@app/hooks/api";
|
||||
import { useCreateLDAPConfig, useGetLDAPConfig, useUpdateLDAPConfig } from "@app/hooks/api";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
const LDAPFormSchema = z.object({
|
||||
@ -19,9 +14,6 @@ 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()
|
||||
});
|
||||
|
||||
@ -35,25 +27,15 @@ type Props = {
|
||||
|
||||
export const LDAPModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props) => {
|
||||
const { currentOrg } = useOrganization();
|
||||
|
||||
|
||||
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, watch } = useForm<TLDAPFormData>({
|
||||
const { control, handleSubmit, reset } = 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({
|
||||
@ -61,25 +43,12 @@ 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 ?? ""
|
||||
});
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
const onSSOModalSubmit = async ({
|
||||
url,
|
||||
bindDN,
|
||||
bindPass,
|
||||
searchBase,
|
||||
searchFilter,
|
||||
groupSearchBase,
|
||||
groupSearchFilter,
|
||||
caCert,
|
||||
shouldCloseModal = true
|
||||
}: TLDAPFormData & { shouldCloseModal?: boolean }) => {
|
||||
const onSSOModalSubmit = async ({ url, bindDN, bindPass, searchBase, caCert }: TLDAPFormData) => {
|
||||
try {
|
||||
if (!currentOrg) return;
|
||||
|
||||
@ -91,9 +60,6 @@ export const LDAPModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props)
|
||||
bindDN,
|
||||
bindPass,
|
||||
searchBase,
|
||||
searchFilter,
|
||||
groupSearchBase,
|
||||
groupSearchFilter,
|
||||
caCert
|
||||
});
|
||||
} else {
|
||||
@ -104,16 +70,11 @@ export const LDAPModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props)
|
||||
bindDN,
|
||||
bindPass,
|
||||
searchBase,
|
||||
searchFilter,
|
||||
groupSearchBase,
|
||||
groupSearchFilter,
|
||||
caCert
|
||||
});
|
||||
}
|
||||
|
||||
if (shouldCloseModal) {
|
||||
handlePopUpClose("addLDAP");
|
||||
}
|
||||
handlePopUpClose("addLDAP");
|
||||
|
||||
createNotification({
|
||||
text: `Successfully ${!data ? "added" : "updated"} LDAP configuration`,
|
||||
@ -128,45 +89,6 @@ 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}
|
||||
@ -209,7 +131,7 @@ export const LDAPModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props)
|
||||
name="searchBase"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="User Search Base / User DN"
|
||||
label="Search Base / User DN"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
@ -217,48 +139,6 @@ 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"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Group Search Base / Group DN (Optional)"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
<Input {...field} placeholder="ou=groups,dc=acme,dc=com" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="groupSearchFilter"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Group Filter (Optional)"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="(&(objectClass=posixGroup)(memberUid={{.Username}}))"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="caCert"
|
||||
@ -281,8 +161,12 @@ export const LDAPModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props)
|
||||
>
|
||||
{!data ? "Add" : "Update"}
|
||||
</Button>
|
||||
<Button colorSchema="secondary" onClick={handleTestLDAPConnection}>
|
||||
Test Connection
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
variant="plain"
|
||||
onClick={() => handlePopUpClose("addLDAP")}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
@ -10,20 +10,16 @@ import {
|
||||
import { useCreateLDAPConfig, useGetLDAPConfig, useUpdateLDAPConfig } from "@app/hooks/api";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
import { LDAPGroupMapModal } from "./LDAPGroupMapModal";
|
||||
import { LDAPModal } from "./LDAPModal";
|
||||
|
||||
export const OrgLDAPSection = (): JSX.Element => {
|
||||
const { currentOrg } = useOrganization();
|
||||
const { subscription } = useSubscription();
|
||||
|
||||
|
||||
const { data } = useGetLDAPConfig(currentOrg?.id ?? "");
|
||||
|
||||
const { mutateAsync } = useUpdateLDAPConfig();
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||
"addLDAP",
|
||||
"ldapGroupMap",
|
||||
"deleteLdapGroupMap",
|
||||
"upgradePlan"
|
||||
] as const);
|
||||
|
||||
@ -67,10 +63,7 @@ export const OrgLDAPSection = (): JSX.Element => {
|
||||
url: "",
|
||||
bindDN: "",
|
||||
bindPass: "",
|
||||
searchBase: "",
|
||||
searchFilter: "",
|
||||
groupSearchBase: "",
|
||||
groupSearchFilter: ""
|
||||
searchBase: ""
|
||||
});
|
||||
}
|
||||
|
||||
@ -83,51 +76,21 @@ export const OrgLDAPSection = (): JSX.Element => {
|
||||
}
|
||||
};
|
||||
|
||||
const openLDAPGroupMapModal = () => {
|
||||
if (!subscription?.ldap) {
|
||||
handlePopUpOpen("upgradePlan");
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("ldapGroupMap");
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<hr className="border-mineshaft-600" />
|
||||
<div className="py-4">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<h2 className="text-md text-mineshaft-100">LDAP</h2>
|
||||
<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>
|
||||
<div className="py-4">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<h2 className="text-md text-mineshaft-100">LDAP Group Mappings</h2>
|
||||
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Ldap}>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
onClick={openLDAPGroupMapModal}
|
||||
colorSchema="secondary"
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
<Button onClick={addLDAPBtnClick} colorSchema="secondary" isDisabled={!isAllowed}>
|
||||
Manage
|
||||
</Button>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</div>
|
||||
<p className="text-sm text-mineshaft-300">
|
||||
Manage how LDAP groups are mapped to internal groups in Infisical
|
||||
</p>
|
||||
<p className="text-sm text-mineshaft-300">Manage LDAP authentication configuration</p>
|
||||
</div>
|
||||
{data && (
|
||||
<div className="py-4">
|
||||
@ -156,11 +119,6 @@ export const OrgLDAPSection = (): JSX.Element => {
|
||||
handlePopUpClose={handlePopUpClose}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
/>
|
||||
<LDAPGroupMapModal
|
||||
popUp={popUp}
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
/>
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||
|