Compare commits

..

45 Commits

Author SHA1 Message Date
3ca596d4af Clean LDAP group search impl async/await 2024-04-24 08:15:19 -07:00
1c95b3abe7 Add license check for ldap group maps 2024-04-23 21:57:40 -07:00
1f3c72b997 Update def features 2024-04-23 21:52:46 -07:00
e55b981cea Merge remote-tracking branch 'origin' into groups-phase-3 2024-04-23 21:47:22 -07:00
49d4e67e07 Smoothen name prefill LDAP 2024-04-23 21:38:51 -07:00
a54d156bf0 Patch LDAP issue 2024-04-23 21:16:55 -07:00
f3fc898232 Add docs for LDAP groups 2024-04-23 19:37:26 -07:00
c61602370e Update kubernetes-helm.mdx 2024-04-23 19:32:26 -07:00
5178663797 Merge pull request #1728 from Infisical/daniel/cli-get-folders-improvement
Feat: Allow "secrets folders get" command to be used with service token & universal auth
2024-04-24 02:46:20 +02:00
f04f3aee25 Fix: Allow service token & UA access token to be used as authentication 2024-04-24 02:36:29 +02:00
e5333e2718 Fix: UA token being overwritten by service token 2024-04-24 02:07:45 +02:00
f27d9f8cee Update release_build_infisical_cli.yml 2024-04-24 00:21:46 +02:00
cbd568b714 Update release_build_infisical_cli.yml 2024-04-24 00:18:25 +02:00
b330c5570d Allow trigger through Github UI 2024-04-24 00:06:35 +02:00
d222bbf131 Update ldap group mapping schema, replace group input field with select 2024-04-23 15:04:02 -07:00
961c6391a8 Complete LDAP group mapping data structure + frontend/backend 2024-04-23 13:58:23 -07:00
d68d7df0f8 Merge pull request #1725 from Infisical/daniel/workflow-env-bug
Fix: Undefined CLI tests env variables
2024-04-23 16:25:43 -04:00
c44c7810ce Fix: CLI Tests failing when called as a dependency workflow 2024-04-23 22:24:17 +02:00
b7893a6a72 Update test-workflow.yml 2024-04-23 22:21:32 +02:00
7a3d425b0e Fix: Undefined env variables 2024-04-23 22:20:43 +02:00
bd570bd02f Merge pull request #1724 from Infisical/daniel/cli-token-bug
Fix: UA Token being overwritten by INFISICAL_TOKEN env variable
2024-04-23 16:07:40 -04:00
b94ffb8a82 Fix: UA Token being overwritten by INFISICAL_TOKEN env variable 2024-04-23 22:00:32 +02:00
246b8728a4 add patroni gha 2024-04-23 14:49:12 -04:00
00415e1a87 Merge pull request #1723 from Infisical/update-folder-error-mg
Update folder not found error msg
2024-04-23 23:28:37 +05:30
ad354c106e update folder not found error message 2024-04-23 13:56:12 -04:00
b067751027 Merge pull request #1720 from Infisical/docs/amplify-patch
docs: added -y flag in infisical cli installation in amplify doc to skip confirmation prompt
2024-04-22 22:54:38 -07:00
f2b3b7b726 docs: added -y flag in infisical cli installation in amplify doc to skip confirmation prompt 2024-04-23 11:23:03 +05:30
2d51445dd9 Add ldapjs to get user groups upon ldap login 2024-04-22 22:02:12 -07:00
1ba7a31e0d Merge pull request #1719 from Infisical/daniel/migration-fix
Fix: Duplicate org membership migration
2024-04-22 19:51:21 -04:00
08cb105fe4 Merge pull request #1712 from akhilmhdh/feat/batch-raw-secrets-api
Batch raw secrets api
2024-04-22 18:48:14 -04:00
62aebe2fd4 Merge pull request #1705 from Infisical/groups-phase-2b
Groups Phase 2A (Pending Group Additions)
2024-04-22 14:25:36 -07:00
5c0542c5a3 Merge remote-tracking branch 'origin' into groups-phase-2b 2024-04-22 14:21:21 -07:00
b9d35058bf Merge remote-tracking branch 'origin' into groups-phase-2b 2024-04-22 12:08:10 -07:00
8c566a5ff7 Fix wording for group project addition/removal 2024-04-22 12:01:29 -07:00
0a124093d6 Patch adding groups to project for invited users, add transactions for adding/removing groups to/from projects 2024-04-22 11:59:17 -07:00
c72280e9ab Merge remote-tracking branch 'origin' into groups-phase-2b 2024-04-22 10:37:53 -07:00
032c5b5620 Convert pending group addition table into isPending field 2024-04-22 10:37:24 -07:00
aa5cd0fd0f feat(server): switched from workspace id to project slug 2024-04-22 21:19:06 +05:30
a339c473d5 docs: updated api doc with bulk raw secret ops 2024-04-19 20:54:41 +05:30
718cabe49b feat(server): added batch raw bulk secret ops api 2024-04-19 20:53:54 +05:30
1dd451f221 Update groups count fn, type check 2024-04-18 14:54:02 -07:00
e1407cc093 Add comments for group-fns 2024-04-18 14:14:08 -07:00
1b38d969df Merge remote-tracking branch 'origin' into groups-phase-2b 2024-04-18 13:52:54 -07:00
6e3d5a8c7c Remove print statements, cleanup 2024-04-18 13:51:47 -07:00
fa7587900e Finish preliminary capability for adding incomplete users to groups 2024-04-18 10:57:25 -07:00
67 changed files with 3155 additions and 660 deletions

View File

@ -0,0 +1,38 @@
name: Build patroni
on: [workflow_dispatch]
jobs:
patroni-image:
name: Build patroni
runs-on: ubuntu-latest
steps:
- name: ☁️ Checkout source
uses: actions/checkout@v3
with:
repository: 'zalando/patroni'
- name: Save commit hashes for tag
id: commit
uses: pr-mpt/actions-commit-hash@v2
- name: 🔧 Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: 🐋 Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up Depot CLI
uses: depot/setup-action@v1
- name: 🏗️ Build backend and push to docker hub
uses: depot/build-push-action@v1
with:
project: 64mmf0n610
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
push: true
context: .
file: Dockerfile
tags: |
infisical/patroni:${{ steps.commit.outputs.short }}
infisical/patroni:latest
platforms: linux/amd64,linux/arm64

View File

@ -1,6 +1,8 @@
name: Build and release CLI
on:
workflow_dispatch:
push:
# run only against tags
tags:
@ -14,6 +16,12 @@ 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

View File

@ -6,7 +6,20 @@ 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:

View File

@ -942,6 +942,113 @@ describe.each([{ auth: AuthMode.JWT }, { auth: AuthMode.IDENTITY_ACCESS_TOKEN }]
const secrets = await getSecrets(seedData1.environment.slug, path);
expect(secrets).toEqual([]);
});
test.each(testRawSecrets)("Bulk create secret raw in path $path", async ({ path, secret }) => {
const createSecretReqBody = {
projectSlug: seedData1.project.slug,
environment: seedData1.environment.slug,
secretPath: path,
secrets: [
{
secretKey: secret.key,
secretValue: secret.value,
secretComment: secret.comment
}
]
};
const createSecRes = await testServer.inject({
method: "POST",
url: `/api/v3/secrets/batch/raw`,
headers: {
authorization: `Bearer ${authToken}`
},
body: createSecretReqBody
});
expect(createSecRes.statusCode).toBe(200);
const createdSecretPayload = JSON.parse(createSecRes.payload);
expect(createdSecretPayload).toHaveProperty("secrets");
// fetch secrets
const secrets = await getSecrets(seedData1.environment.slug, path);
expect(secrets).toEqual(
expect.arrayContaining([
expect.objectContaining({
key: secret.key,
value: secret.value,
type: SecretType.Shared
})
])
);
await deleteRawSecret({ path, key: secret.key });
});
test.each(testRawSecrets)("Bulk update secret raw in path $path", async ({ secret, path }) => {
await createRawSecret({ path, ...secret });
const updateSecretReqBody = {
projectSlug: seedData1.project.slug,
environment: seedData1.environment.slug,
secretPath: path,
secrets: [
{
secretValue: "new-value",
secretKey: secret.key
}
]
};
const updateSecRes = await testServer.inject({
method: "PATCH",
url: `/api/v3/secrets/batch/raw`,
headers: {
authorization: `Bearer ${authToken}`
},
body: updateSecretReqBody
});
expect(updateSecRes.statusCode).toBe(200);
const updatedSecretPayload = JSON.parse(updateSecRes.payload);
expect(updatedSecretPayload).toHaveProperty("secrets");
// fetch secrets
const secrets = await getSecrets(seedData1.environment.slug, path);
expect(secrets).toEqual(
expect.arrayContaining([
expect.objectContaining({
key: secret.key,
value: "new-value",
version: 2,
type: SecretType.Shared
})
])
);
await deleteRawSecret({ path, key: secret.key });
});
test.each(testRawSecrets)("Bulk delete secret raw in path $path", async ({ path, secret }) => {
await createRawSecret({ path, ...secret });
const deletedSecretReqBody = {
projectSlug: seedData1.project.slug,
environment: seedData1.environment.slug,
secretPath: path,
secrets: [{ secretKey: secret.key }]
};
const deletedSecRes = await testServer.inject({
method: "DELETE",
url: `/api/v3/secrets/batch/raw`,
headers: {
authorization: `Bearer ${authToken}`
},
body: deletedSecretReqBody
});
expect(deletedSecRes.statusCode).toBe(200);
const deletedSecretPayload = JSON.parse(deletedSecRes.payload);
expect(deletedSecretPayload).toHaveProperty("secrets");
// fetch secrets
const secrets = await getSecrets(seedData1.environment.slug, path);
expect(secrets).toEqual([]);
});
}
);

View File

@ -45,6 +45,7 @@
"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",
@ -2510,6 +2511,83 @@
"@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",
@ -9304,15 +9382,7 @@
"node": ">=0.8.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": {
"node_modules/ldapauth-fork/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==",
@ -9330,6 +9400,35 @@
"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",

View File

@ -106,6 +106,7 @@
"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",

View File

@ -74,6 +74,9 @@ import {
TLdapConfigs,
TLdapConfigsInsert,
TLdapConfigsUpdate,
TLdapGroupMaps,
TLdapGroupMapsInsert,
TLdapGroupMapsUpdate,
TOrganizations,
TOrganizationsInsert,
TOrganizationsUpdate,
@ -398,6 +401,7 @@ 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<

View File

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

View File

@ -0,0 +1,34 @@
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");
});
}

View File

@ -22,6 +22,7 @@ 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";

View File

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

View File

@ -0,0 +1,19 @@
// 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>>;

View File

@ -60,6 +60,7 @@ 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",

View File

@ -12,7 +12,8 @@ export const UserGroupMembershipSchema = z.object({
userId: z.string().uuid(),
groupId: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date()
updatedAt: z.date(),
isPending: z.boolean().default(false)
});
export type TUserGroupMembership = z.infer<typeof UserGroupMembershipSchema>;

View File

@ -14,7 +14,9 @@ import { FastifyRequest } from "fastify";
import LdapStrategy from "passport-ldapauth";
import { z } from "zod";
import { LdapConfigsSchema } from "@app/db/schemas";
import { LdapConfigsSchema, LdapGroupMapsSchema } from "@app/db/schemas";
import { TLDAPConfig } from "@app/ee/services/ldap-config/ldap-config-types";
import { searchGroups } from "@app/ee/services/ldap-config/ldap-fns";
import { getConfig } from "@app/lib/config/env";
import { logger } from "@app/lib/logger";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
@ -50,20 +52,33 @@ 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;
const groupFilter = "(|(memberUid={{.Username}})(member={{.UserDN}})(uniqueMember={{.UserDN}}))";
const searchFilter =
ldapConfig.groupSearchFilter ||
groupFilter.replace("{{.Username}}", user.uid).replace("{{.UserDN}}", user.dn);
const shouldProcessGroups = ldapConfig.groupSearchFilter && ldapConfig.groupSearchBase;
const { isUserCompleted, providerAuthToken } = await server.services.ldap.ldapLogin({
ldapConfigId: ldapConfig.id,
externalId: user.uidNumber,
username: user.uid,
firstName: user.givenName,
lastName: user.sn,
firstName: user.givenName ?? user.cn ?? "",
lastName: user.sn ?? "",
emails: user.mail ? [user.mail] : [],
groups: shouldProcessGroups
? await searchGroups(ldapConfig, searchFilter, ldapConfig.groupSearchBase)
: undefined,
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 (err) {
logger.error(err);
return cb(err, false);
} catch (error) {
logger.error(error);
return cb(error, false);
}
}
)
@ -117,6 +132,8 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => {
bindDN: z.string(),
bindPass: z.string(),
searchBase: z.string(),
groupSearchBase: z.string(),
groupSearchFilter: z.string(),
caCert: z.string()
})
}
@ -148,6 +165,8 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => {
bindDN: z.string().trim(),
bindPass: z.string().trim(),
searchBase: z.string().trim(),
groupSearchBase: z.string().trim().default(""),
groupSearchFilter: z.string().trim().default(""),
caCert: z.string().trim().default("")
}),
response: {
@ -183,6 +202,8 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => {
bindDN: z.string().trim(),
bindPass: z.string().trim(),
searchBase: z.string().trim(),
groupSearchBase: z.string().trim(),
groupSearchFilter: z.string().trim(),
caCert: z.string().trim()
})
.partial()
@ -204,4 +225,106 @@ 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;
}
});
};

View File

@ -289,14 +289,28 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
body: z.object({
schemas: z.array(z.string()),
displayName: z.string().trim(),
members: z.array(z.any()).length(0).optional() // okta-specific
members: z
.array(
z.object({
value: z.string(),
display: z.string()
})
)
.optional() // okta-specific
}),
response: {
200: z.object({
schemas: z.array(z.string()),
id: z.string().trim(),
displayName: z.string().trim(),
members: z.array(z.any()).length(0),
members: z
.array(
z.object({
value: z.string(),
display: z.string()
})
)
.optional(),
meta: z.object({
resourceType: z.string().trim()
})
@ -306,8 +320,8 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
handler: async (req) => {
const group = await req.server.services.scim.createScimGroup({
displayName: req.body.displayName,
orgId: req.permission.orgId
orgId: req.permission.orgId,
...req.body
});
return group;
@ -400,7 +414,12 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
schemas: z.array(z.string()),
id: z.string().trim(),
displayName: z.string().trim(),
members: z.array(z.any()).length(0)
members: z.array(
z.object({
value: z.string(), // infisical userId
display: z.string()
})
) // note: is this where members are added to group?
}),
response: {
200: z.object({
@ -424,7 +443,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
const group = await req.server.services.scim.updateScimGroupNamePut({
groupId: req.params.groupId,
orgId: req.permission.orgId,
displayName: req.body.displayName
...req.body
});
return group;
@ -482,8 +501,6 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
},
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
handler: async (req) => {
// console.log("PATCH /Groups/:groupId req.body: ", req.body);
// console.log("PATCH /Groups/:groupId req.body: ", req.body.Operations[0]);
const group = await req.server.services.scim.updateScimGroupNamePatch({
groupId: req.params.groupId,
orgId: req.permission.orgId,

View File

@ -59,32 +59,6 @@ export const groupDALFactory = (db: TDbClient) => {
}
};
const countAllGroupMembers = async ({ orgId, groupId }: { orgId: string; groupId: string }) => {
try {
interface CountResult {
count: string;
}
const doc = await db<CountResult>(TableName.OrgMembership)
.where(`${TableName.OrgMembership}.orgId`, orgId)
.join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`)
.leftJoin(TableName.UserGroupMembership, function () {
this.on(`${TableName.UserGroupMembership}.userId`, "=", `${TableName.Users}.id`).andOn(
`${TableName.UserGroupMembership}.groupId`,
"=",
db.raw("?", [groupId])
);
})
.where({ isGhost: false })
.count(`${TableName.Users}.id`)
.first();
return parseInt((doc?.count as string) || "0", 10);
} catch (err) {
throw new DatabaseError({ error: err, name: "Count all group members" });
}
};
// special query
const findAllGroupMembers = async ({
orgId,
@ -150,7 +124,6 @@ export const groupDALFactory = (db: TDbClient) => {
return {
findGroups,
findByOrgId,
countAllGroupMembers,
findAllGroupMembers,
...groupOrm
};

View File

@ -0,0 +1,450 @@
import { Knex } from "knex";
import { SecretKeyEncoding, TUsers } from "@app/db/schemas";
import { decryptAsymmetric, encryptAsymmetric, infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
import { BadRequestError, ScimRequestError } from "@app/lib/errors";
import {
TAddUsersToGroup,
TAddUsersToGroupByUserIds,
TConvertPendingGroupAdditionsToGroupMemberships,
TRemoveUsersFromGroupByUserIds
} from "./group-types";
const addAcceptedUsersToGroup = async ({
userIds,
group,
userGroupMembershipDAL,
userDAL,
groupProjectDAL,
projectKeyDAL,
projectDAL,
projectBotDAL,
tx
}: TAddUsersToGroup) => {
const users = await userDAL.findUserEncKeyByUserIdsBatch(
{
userIds
},
tx
);
await userGroupMembershipDAL.insertMany(
users.map((user) => ({
userId: user.userId,
groupId: group.id,
isPending: false
})),
tx
);
// check which projects the group is part of
const projectIds = Array.from(
new Set(
(
await groupProjectDAL.find(
{
groupId: group.id
},
{ tx }
)
).map((gp) => gp.projectId)
)
);
const keys = await projectKeyDAL.find(
{
$in: {
projectId: projectIds,
receiverId: users.map((u) => u.id)
}
},
{ tx }
);
const userKeysSet = new Set(keys.map((k) => `${k.projectId}-${k.receiverId}`));
for await (const projectId of projectIds) {
const usersToAddProjectKeyFor = users.filter((u) => !userKeysSet.has(`${projectId}-${u.userId}`));
if (usersToAddProjectKeyFor.length) {
// there are users who need to be shared keys
// process adding bulk users to projects for each project individually
const ghostUser = await projectDAL.findProjectGhostUser(projectId, tx);
if (!ghostUser) {
throw new BadRequestError({
message: "Failed to find sudo user"
});
}
const ghostUserLatestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.id, projectId, tx);
if (!ghostUserLatestKey) {
throw new BadRequestError({
message: "Failed to find sudo user latest key"
});
}
const bot = await projectBotDAL.findOne({ projectId }, tx);
if (!bot) {
throw new BadRequestError({
message: "Failed to find bot"
});
}
const botPrivateKey = infisicalSymmetricDecrypt({
keyEncoding: bot.keyEncoding as SecretKeyEncoding,
iv: bot.iv,
tag: bot.tag,
ciphertext: bot.encryptedPrivateKey
});
const plaintextProjectKey = decryptAsymmetric({
ciphertext: ghostUserLatestKey.encryptedKey,
nonce: ghostUserLatestKey.nonce,
publicKey: ghostUserLatestKey.sender.publicKey,
privateKey: botPrivateKey
});
const projectKeysToAdd = usersToAddProjectKeyFor.map((user) => {
const { ciphertext: encryptedKey, nonce } = encryptAsymmetric(
plaintextProjectKey,
user.publicKey,
botPrivateKey
);
return {
encryptedKey,
nonce,
senderId: ghostUser.id,
receiverId: user.userId,
projectId
};
});
await projectKeyDAL.insertMany(projectKeysToAdd, tx);
}
}
};
/**
* Add users with user ids [userIds] to group [group].
* - Users may or may not have finished completing their accounts; this function will
* handle both adding users to groups directly and via pending group additions.
* @param {group} group - group to add user(s) to
* @param {string[]} userIds - id(s) of user(s) to add to group
*/
export const addUsersToGroupByUserIds = async ({
group,
userIds,
userDAL,
userGroupMembershipDAL,
orgDAL,
groupProjectDAL,
projectKeyDAL,
projectDAL,
projectBotDAL,
tx: outerTx
}: TAddUsersToGroupByUserIds) => {
const processAddition = async (tx: Knex) => {
const foundMembers = await userDAL.find(
{
$in: {
id: userIds
}
},
{ tx }
);
const foundMembersIdsSet = new Set(foundMembers.map((member) => member.id));
const isCompleteMatch = userIds.every((userId) => foundMembersIdsSet.has(userId));
if (!isCompleteMatch) {
throw new ScimRequestError({
detail: "Members not found",
status: 404
});
}
// check if user(s) group membership(s) already exists
const existingUserGroupMemberships = await userGroupMembershipDAL.find(
{
groupId: group.id,
$in: {
userId: userIds
}
},
{ tx }
);
if (existingUserGroupMemberships.length) {
throw new BadRequestError({
message: `User(s) are already part of the group ${group.slug}`
});
}
// check if all user(s) are part of the organization
const existingUserOrgMemberships = await orgDAL.findMembership(
{
orgId: group.orgId,
$in: {
userId: userIds
}
},
{ tx }
);
const existingUserOrgMembershipsUserIdsSet = new Set(existingUserOrgMemberships.map((u) => u.userId));
userIds.forEach((userId) => {
if (!existingUserOrgMembershipsUserIdsSet.has(userId))
throw new BadRequestError({
message: `User with id ${userId} is not part of the organization`
});
});
const membersToAddToGroupNonPending: TUsers[] = [];
const membersToAddToGroupPending: TUsers[] = [];
foundMembers.forEach((member) => {
if (member.isAccepted) {
// add accepted member to group
membersToAddToGroupNonPending.push(member);
} else {
// add incomplete member to pending group addition
membersToAddToGroupPending.push(member);
}
});
if (membersToAddToGroupNonPending.length) {
await addAcceptedUsersToGroup({
userIds: membersToAddToGroupNonPending.map((member) => member.id),
group,
userDAL,
userGroupMembershipDAL,
groupProjectDAL,
projectKeyDAL,
projectDAL,
projectBotDAL,
tx
});
}
if (membersToAddToGroupPending.length) {
await userGroupMembershipDAL.insertMany(
membersToAddToGroupPending.map((member) => ({
userId: member.id,
groupId: group.id,
isPending: true
})),
tx
);
}
return membersToAddToGroupNonPending.concat(membersToAddToGroupPending);
};
if (outerTx) {
return processAddition(outerTx);
}
return userDAL.transaction(async (tx) => {
return processAddition(tx);
});
};
/**
* Remove users with user ids [userIds] from group [group].
* - Users may be part of the group (non-pending + pending);
* this function will handle both cases.
* @param {group} group - group to remove user(s) from
* @param {string[]} userIds - id(s) of user(s) to remove from group
*/
export const removeUsersFromGroupByUserIds = async ({
group,
userIds,
userDAL,
userGroupMembershipDAL,
groupProjectDAL,
projectKeyDAL,
tx: outerTx
}: TRemoveUsersFromGroupByUserIds) => {
const processRemoval = async (tx: Knex) => {
const foundMembers = await userDAL.find({
$in: {
id: userIds
}
});
const foundMembersIdsSet = new Set(foundMembers.map((member) => member.id));
const isCompleteMatch = userIds.every((userId) => foundMembersIdsSet.has(userId));
if (!isCompleteMatch) {
throw new ScimRequestError({
detail: "Members not found",
status: 404
});
}
// check if user group membership already exists
const existingUserGroupMemberships = await userGroupMembershipDAL.find(
{
groupId: group.id,
$in: {
userId: userIds
}
},
{ tx }
);
const existingUserGroupMembershipsUserIdsSet = new Set(existingUserGroupMemberships.map((u) => u.userId));
userIds.forEach((userId) => {
if (!existingUserGroupMembershipsUserIdsSet.has(userId))
throw new BadRequestError({
message: `User(s) are not part of the group ${group.slug}`
});
});
const membersToRemoveFromGroupNonPending: TUsers[] = [];
const membersToRemoveFromGroupPending: TUsers[] = [];
foundMembers.forEach((member) => {
if (member.isAccepted) {
// remove accepted member from group
membersToRemoveFromGroupNonPending.push(member);
} else {
// remove incomplete member from pending group addition
membersToRemoveFromGroupPending.push(member);
}
});
if (membersToRemoveFromGroupNonPending.length) {
// check which projects the group is part of
const projectIds = Array.from(
new Set(
(
await groupProjectDAL.find(
{
groupId: group.id
},
{ tx }
)
).map((gp) => gp.projectId)
)
);
// TODO: this part can be optimized
for await (const userId of userIds) {
const t = await userGroupMembershipDAL.filterProjectsByUserMembership(userId, group.id, projectIds, tx);
const projectsToDeleteKeyFor = projectIds.filter((p) => !t.has(p));
if (projectsToDeleteKeyFor.length) {
await projectKeyDAL.delete(
{
receiverId: userId,
$in: {
projectId: projectsToDeleteKeyFor
}
},
tx
);
}
await userGroupMembershipDAL.delete(
{
groupId: group.id,
userId
},
tx
);
}
}
if (membersToRemoveFromGroupPending.length) {
await userGroupMembershipDAL.delete({
groupId: group.id,
$in: {
userId: membersToRemoveFromGroupPending.map((member) => member.id)
}
});
}
return membersToRemoveFromGroupNonPending.concat(membersToRemoveFromGroupPending);
};
if (outerTx) {
return processRemoval(outerTx);
}
return userDAL.transaction(async (tx) => {
return processRemoval(tx);
});
};
/**
* Convert pending group additions for users with ids [userIds] to group memberships.
* @param {string[]} userIds - id(s) of user(s) to try to convert pending group additions to group memberships
*/
export const convertPendingGroupAdditionsToGroupMemberships = async ({
userIds,
userDAL,
userGroupMembershipDAL,
groupProjectDAL,
projectKeyDAL,
projectDAL,
projectBotDAL,
tx: outerTx
}: TConvertPendingGroupAdditionsToGroupMemberships) => {
const processConversion = async (tx: Knex) => {
const users = await userDAL.find(
{
$in: {
id: userIds
}
},
{ tx }
);
const usersUserIdsSet = new Set(users.map((u) => u.id));
userIds.forEach((userId) => {
if (!usersUserIdsSet.has(userId)) {
throw new BadRequestError({
message: `Failed to find user with id ${userId}`
});
}
});
users.forEach((user) => {
if (!user.isAccepted) {
throw new BadRequestError({
message: `Failed to convert pending group additions to group memberships for user ${user.username} because they have not confirmed their account`
});
}
});
const pendingGroupAdditions = await userGroupMembershipDAL.deletePendingUserGroupMembershipsByUserIds(userIds, tx);
for await (const pendingGroupAddition of pendingGroupAdditions) {
await addAcceptedUsersToGroup({
userIds: [pendingGroupAddition.user.id],
group: pendingGroupAddition.group,
userDAL,
userGroupMembershipDAL,
groupProjectDAL,
projectKeyDAL,
projectDAL,
projectBotDAL,
tx
});
}
};
if (outerTx) {
return processConversion(outerTx);
}
return userDAL.transaction(async (tx) => {
await processConversion(tx);
});
};

View File

@ -1,22 +1,22 @@
import { ForbiddenError } from "@casl/ability";
import slugify from "@sindresorhus/slugify";
import { OrgMembershipRole, SecretKeyEncoding, TOrgRoles } from "@app/db/schemas";
import { OrgMembershipRole, TOrgRoles } from "@app/db/schemas";
import { isAtLeastAsPrivileged } from "@app/lib/casl";
import { decryptAsymmetric, encryptAsymmetric, infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-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 { TGroupProjectDALFactory } from "../../../services/group-project/group-project-dal";
import { TOrgDALFactory } from "../../../services/org/org-dal";
import { TProjectDALFactory } from "../../../services/project/project-dal";
import { TProjectBotDALFactory } from "../../../services/project-bot/project-bot-dal";
import { TProjectKeyDALFactory } from "../../../services/project-key/project-key-dal";
import { TUserDALFactory } from "../../../services/user/user-dal";
import { TLicenseServiceFactory } from "../license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
import { TPermissionServiceFactory } from "../permission/permission-service";
import { TGroupDALFactory } from "./group-dal";
import { addUsersToGroupByUserIds, removeUsersFromGroupByUserIds } from "./group-fns";
import {
TAddUserToGroupDTO,
TCreateGroupDTO,
@ -28,20 +28,17 @@ import {
import { TUserGroupMembershipDALFactory } from "./user-group-membership-dal";
type TGroupServiceFactoryDep = {
userDAL: Pick<TUserDALFactory, "findOne" | "findUserEncKeyByUsername">;
groupDAL: Pick<
TGroupDALFactory,
"create" | "findOne" | "update" | "delete" | "findAllGroupMembers" | "countAllGroupMembers"
>;
userDAL: Pick<TUserDALFactory, "find" | "findUserEncKeyByUserIdsBatch" | "transaction" | "findOne">;
groupDAL: Pick<TGroupDALFactory, "create" | "findOne" | "update" | "delete" | "findAllGroupMembers">;
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
orgDAL: Pick<TOrgDALFactory, "findMembership">;
orgDAL: Pick<TOrgDALFactory, "findMembership" | "countAllOrgMembers">;
userGroupMembershipDAL: Pick<
TUserGroupMembershipDALFactory,
"findOne" | "create" | "delete" | "filterProjectsByUserMembership"
"findOne" | "delete" | "filterProjectsByUserMembership" | "transaction" | "insertMany" | "find"
>;
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "create" | "delete" | "findLatestProjectKey">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete" | "findLatestProjectKey" | "insertMany">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getOrgPermissionByRole">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
};
@ -227,12 +224,9 @@ export const groupServiceFactory = ({
username
});
const totalCount = await groupDAL.countAllGroupMembers({
orgId: group.orgId,
groupId: group.id
});
const count = await orgDAL.countAllOrgMembers(group.orgId);
return { users, totalCount };
return { users, totalCount: count };
};
const addUserToGroup = async ({
@ -272,111 +266,22 @@ export const groupServiceFactory = ({
if (!hasRequiredPriviledges)
throw new ForbiddenRequestError({ message: "Failed to add user to more privileged group" });
// get user with username
const user = await userDAL.findUserEncKeyByUsername({
username
const user = await userDAL.findOne({ username });
if (!user) throw new BadRequestError({ message: `Failed to find user with username ${username}` });
const users = await addUsersToGroupByUserIds({
group,
userIds: [user.id],
userDAL,
userGroupMembershipDAL,
orgDAL,
groupProjectDAL,
projectKeyDAL,
projectDAL,
projectBotDAL
});
if (!user)
throw new BadRequestError({
message: `Failed to find user with username ${username}`
});
// check if user group membership already exists
const existingUserGroupMembership = await userGroupMembershipDAL.findOne({
groupId: group.id,
userId: user.userId
});
if (existingUserGroupMembership)
throw new BadRequestError({
message: `User ${username} is already part of the group ${groupSlug}`
});
// check if user is even part of the organization
const existingUserOrgMembership = await orgDAL.findMembership({
userId: user.userId,
orgId: actorOrgId
});
if (!existingUserOrgMembership)
throw new BadRequestError({
message: `User ${username} is not part of the organization`
});
await userGroupMembershipDAL.create({
userId: user.userId,
groupId: group.id
});
// check which projects the group is part of
const projectIds = (
await groupProjectDAL.find({
groupId: group.id
})
).map((gp) => gp.projectId);
const keys = await projectKeyDAL.find({
receiverId: user.userId,
$in: {
projectId: projectIds
}
});
const keysSet = new Set(keys.map((k) => k.projectId));
const projectsToAddKeyFor = projectIds.filter((p) => !keysSet.has(p));
for await (const projectId of projectsToAddKeyFor) {
const ghostUser = await projectDAL.findProjectGhostUser(projectId);
if (!ghostUser) {
throw new BadRequestError({
message: "Failed to find sudo user"
});
}
const ghostUserLatestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.id, projectId);
if (!ghostUserLatestKey) {
throw new BadRequestError({
message: "Failed to find sudo user latest key"
});
}
const bot = await projectBotDAL.findOne({ projectId });
if (!bot) {
throw new BadRequestError({
message: "Failed to find bot"
});
}
const botPrivateKey = infisicalSymmetricDecrypt({
keyEncoding: bot.keyEncoding as SecretKeyEncoding,
iv: bot.iv,
tag: bot.tag,
ciphertext: bot.encryptedPrivateKey
});
const plaintextProjectKey = decryptAsymmetric({
ciphertext: ghostUserLatestKey.encryptedKey,
nonce: ghostUserLatestKey.nonce,
publicKey: ghostUserLatestKey.sender.publicKey,
privateKey: botPrivateKey
});
const { ciphertext: encryptedKey, nonce } = encryptAsymmetric(plaintextProjectKey, user.publicKey, botPrivateKey);
await projectKeyDAL.create({
encryptedKey,
nonce,
senderId: ghostUser.id,
receiverId: user.userId,
projectId
});
}
return user;
return users[0];
};
const removeUserFromGroup = async ({
@ -416,51 +321,19 @@ export const groupServiceFactory = ({
if (!hasRequiredPriviledges)
throw new ForbiddenRequestError({ message: "Failed to delete user from more privileged group" });
const user = await userDAL.findOne({
username
const user = await userDAL.findOne({ username });
if (!user) throw new BadRequestError({ message: `Failed to find user with username ${username}` });
const users = await removeUsersFromGroupByUserIds({
group,
userIds: [user.id],
userDAL,
userGroupMembershipDAL,
groupProjectDAL,
projectKeyDAL
});
if (!user)
throw new BadRequestError({
message: `Failed to find user with username ${username}`
});
// check if user group membership already exists
const existingUserGroupMembership = await userGroupMembershipDAL.findOne({
groupId: group.id,
userId: user.id
});
if (!existingUserGroupMembership)
throw new BadRequestError({
message: `User ${username} is not part of the group ${groupSlug}`
});
const projectIds = (
await groupProjectDAL.find({
groupId: group.id
})
).map((gp) => gp.projectId);
const t = await userGroupMembershipDAL.filterProjectsByUserMembership(user.id, group.id, projectIds);
const projectsToDeleteKeyFor = projectIds.filter((p) => !t.has(p));
if (projectsToDeleteKeyFor.length) {
await projectKeyDAL.delete({
receiverId: user.id,
$in: {
projectId: projectsToDeleteKeyFor
}
});
}
await userGroupMembershipDAL.delete({
groupId: group.id,
userId: user.id
});
return user;
return users[0];
};
return {

View File

@ -1,4 +1,14 @@
import { Knex } from "knex";
import { TGroups } from "@app/db/schemas";
import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
import { TGenericPermission } from "@app/lib/types";
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-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";
export type TCreateGroupDTO = {
name: string;
@ -35,3 +45,54 @@ export type TRemoveUserFromGroupDTO = {
groupSlug: string;
username: string;
} & TGenericPermission;
// group fns types
export type TAddUsersToGroup = {
userIds: string[];
group: TGroups;
userDAL: Pick<TUserDALFactory, "findUserEncKeyByUserIdsBatch">;
userGroupMembershipDAL: Pick<TUserGroupMembershipDALFactory, "find" | "transaction" | "insertMany">;
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany">;
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
tx: Knex;
};
export type TAddUsersToGroupByUserIds = {
group: TGroups;
userIds: string[];
userDAL: Pick<TUserDALFactory, "find" | "findUserEncKeyByUserIdsBatch" | "transaction">;
userGroupMembershipDAL: Pick<TUserGroupMembershipDALFactory, "find" | "transaction" | "insertMany">;
orgDAL: Pick<TOrgDALFactory, "findMembership">;
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany">;
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
tx?: Knex;
};
export type TRemoveUsersFromGroupByUserIds = {
group: TGroups;
userIds: string[];
userDAL: Pick<TUserDALFactory, "find" | "transaction">;
userGroupMembershipDAL: Pick<TUserGroupMembershipDALFactory, "find" | "filterProjectsByUserMembership" | "delete">;
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "delete">;
tx?: Knex;
};
export type TConvertPendingGroupAdditionsToGroupMemberships = {
userIds: string[];
userDAL: Pick<TUserDALFactory, "findUserEncKeyByUserIdsBatch" | "transaction" | "find" | "findById">;
userGroupMembershipDAL: Pick<
TUserGroupMembershipDALFactory,
"find" | "transaction" | "insertMany" | "deletePendingUserGroupMembershipsByUserIds"
>;
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany">;
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
tx?: Knex;
};

View File

@ -1,3 +1,5 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import { TableName, TUserEncryptionKeys } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
@ -14,24 +16,28 @@ export const userGroupMembershipDALFactory = (db: TDbClient) => {
* - The user is a member of a group that is a member of the project, excluding projects that they are part of
* through the group with id [groupId].
*/
const filterProjectsByUserMembership = async (userId: string, groupId: string, projectIds: string[]) => {
const userProjectMemberships: string[] = await db(TableName.ProjectMembership)
.where(`${TableName.ProjectMembership}.userId`, userId)
.whereIn(`${TableName.ProjectMembership}.projectId`, projectIds)
.pluck(`${TableName.ProjectMembership}.projectId`);
const filterProjectsByUserMembership = async (userId: string, groupId: string, projectIds: string[], tx?: Knex) => {
try {
const userProjectMemberships: string[] = await (tx || db)(TableName.ProjectMembership)
.where(`${TableName.ProjectMembership}.userId`, userId)
.whereIn(`${TableName.ProjectMembership}.projectId`, projectIds)
.pluck(`${TableName.ProjectMembership}.projectId`);
const userGroupMemberships: string[] = await db(TableName.UserGroupMembership)
.where(`${TableName.UserGroupMembership}.userId`, userId)
.whereNot(`${TableName.UserGroupMembership}.groupId`, groupId)
.join(
TableName.GroupProjectMembership,
`${TableName.UserGroupMembership}.groupId`,
`${TableName.GroupProjectMembership}.groupId`
)
.whereIn(`${TableName.GroupProjectMembership}.projectId`, projectIds)
.pluck(`${TableName.GroupProjectMembership}.projectId`);
const userGroupMemberships: string[] = await (tx || db)(TableName.UserGroupMembership)
.where(`${TableName.UserGroupMembership}.userId`, userId)
.whereNot(`${TableName.UserGroupMembership}.groupId`, groupId)
.join(
TableName.GroupProjectMembership,
`${TableName.UserGroupMembership}.groupId`,
`${TableName.GroupProjectMembership}.groupId`
)
.whereIn(`${TableName.GroupProjectMembership}.projectId`, projectIds)
.pluck(`${TableName.GroupProjectMembership}.projectId`);
return new Set(userProjectMemberships.concat(userGroupMemberships));
return new Set(userProjectMemberships.concat(userGroupMemberships));
} catch (error) {
throw new DatabaseError({ error, name: "Filter projects by user membership" });
}
};
// special query
@ -45,7 +51,7 @@ export const userGroupMembershipDALFactory = (db: TDbClient) => {
)
.join(TableName.Users, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`)
.where(`${TableName.GroupProjectMembership}.projectId`, projectId)
.whereIn(`${TableName.Users}.username`, usernames) // TODO: pluck usernames
.whereIn(`${TableName.Users}.username`, usernames)
.pluck(`${TableName.Users}.id`);
return usernameDocs;
@ -55,7 +61,7 @@ export const userGroupMembershipDALFactory = (db: TDbClient) => {
};
/**
* Return list of users that are part of the group with id [groupId]
* Return list of completed/accepted users that are part of the group with id [groupId]
* that have not yet been added individually to project with id [projectId].
*
* Note: Filters out users that are part of other groups in the project.
@ -63,18 +69,19 @@ export const userGroupMembershipDALFactory = (db: TDbClient) => {
* @param projectId
* @returns
*/
const findGroupMembersNotInProject = async (groupId: string, projectId: string) => {
const findGroupMembersNotInProject = async (groupId: string, projectId: string, tx?: Knex) => {
try {
// get list of groups in the project with id [projectId]
// that that are not the group with id [groupId]
const groups: string[] = await db(TableName.GroupProjectMembership)
const groups: string[] = await (tx || db)(TableName.GroupProjectMembership)
.where(`${TableName.GroupProjectMembership}.projectId`, projectId)
.whereNot(`${TableName.GroupProjectMembership}.groupId`, groupId)
.pluck(`${TableName.GroupProjectMembership}.groupId`);
// main query
const members = await db(TableName.UserGroupMembership)
const members = await (tx || db)(TableName.UserGroupMembership)
.where(`${TableName.UserGroupMembership}.groupId`, groupId)
.where(`${TableName.UserGroupMembership}.isPending`, false)
.join(TableName.Users, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`)
.leftJoin(TableName.ProjectMembership, function () {
this.on(`${TableName.Users}.id`, "=", `${TableName.ProjectMembership}.userId`).andOn(
@ -116,10 +123,49 @@ export const userGroupMembershipDALFactory = (db: TDbClient) => {
}
};
const deletePendingUserGroupMembershipsByUserIds = async (userIds: string[], tx?: Knex) => {
try {
const members = await (tx || db)(TableName.UserGroupMembership)
.whereIn(`${TableName.UserGroupMembership}.userId`, userIds)
.where(`${TableName.UserGroupMembership}.isPending`, true)
.join(TableName.Groups, `${TableName.UserGroupMembership}.groupId`, `${TableName.Groups}.id`)
.join(TableName.Users, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`);
await userGroupMembershipOrm.delete(
{
$in: {
userId: userIds
}
},
tx
);
return members.map(({ userId, username, groupId, orgId, name, slug, role, roleId }) => ({
user: {
id: userId,
username
},
group: {
id: groupId,
orgId,
name,
slug,
role,
roleId,
createdAt: new Date(),
updatedAt: new Date()
}
}));
} catch (error) {
throw new DatabaseError({ error, name: "Delete pending user group memberships by user ids" });
}
};
return {
...userGroupMembershipOrm,
filterProjectsByUserMembership,
findUserGroupMembershipsInProject,
findGroupMembersNotInProject
findGroupMembersNotInProject,
deletePendingUserGroupMembershipsByUserIds
};
};

View File

@ -2,6 +2,9 @@ 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,
@ -13,8 +16,12 @@ 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";
@ -23,16 +30,38 @@ 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, TGetLdapCfgDTO, TLdapLoginDTO, TUpdateLdapCfgDTO } from "./ldap-config-types";
import {
TCreateLdapCfgDTO,
TCreateLdapGroupMapDTO,
TDeleteLdapGroupMapDTO,
TGetLdapCfgDTO,
TGetLdapGroupMapsDTO,
TLdapLoginDTO,
TUpdateLdapCfgDTO
} from "./ldap-config-types";
import { TLdapGroupMapDALFactory } from "./ldap-group-map-dal";
type TLdapConfigServiceFactoryDep = {
ldapConfigDAL: TLdapConfigDALFactory;
ldapConfigDAL: Pick<TLdapConfigDALFactory, "create" | "update" | "findOne">;
ldapGroupMapDAL: Pick<TLdapGroupMapDALFactory, "find" | "create" | "delete" | "findLdapGroupMapsByLdapConfigId">;
orgDAL: Pick<
TOrgDALFactory,
"createMembership" | "updateMembershipById" | "findMembership" | "findOrgById" | "findOne" | "updateById"
>;
orgBotDAL: Pick<TOrgBotDALFactory, "findOne" | "create" | "transaction">;
userDAL: Pick<TUserDALFactory, "create" | "findOne" | "transaction" | "updateById">;
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"
>;
userAliasDAL: Pick<TUserAliasDALFactory, "create" | "findOne">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
@ -42,8 +71,15 @@ export type TLdapConfigServiceFactory = ReturnType<typeof ldapConfigServiceFacto
export const ldapConfigServiceFactory = ({
ldapConfigDAL,
ldapGroupMapDAL,
orgDAL,
orgBotDAL,
groupDAL,
groupProjectDAL,
projectKeyDAL,
projectDAL,
projectBotDAL,
userGroupMembershipDAL,
userDAL,
userAliasDAL,
permissionService,
@ -60,6 +96,8 @@ export const ldapConfigServiceFactory = ({
bindDN,
bindPass,
searchBase,
groupSearchBase,
groupSearchFilter,
caCert
}: TCreateLdapCfgDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
@ -135,6 +173,8 @@ export const ldapConfigServiceFactory = ({
bindPassIV,
bindPassTag,
searchBase,
groupSearchBase,
groupSearchFilter,
encryptedCACert,
caCertIV,
caCertTag
@ -154,6 +194,8 @@ export const ldapConfigServiceFactory = ({
bindDN,
bindPass,
searchBase,
groupSearchBase,
groupSearchFilter,
caCert
}: TUpdateLdapCfgDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
@ -169,7 +211,9 @@ export const ldapConfigServiceFactory = ({
const updateQuery: TLdapConfigsUpdate = {
isActive,
url,
searchBase
searchBase,
groupSearchBase,
groupSearchFilter
};
const orgBot = await orgBotDAL.findOne({ orgId });
@ -271,6 +315,8 @@ export const ldapConfigServiceFactory = ({
bindDN,
bindPass,
searchBase: ldapConfig.searchBase,
groupSearchBase: ldapConfig.groupSearchBase,
groupSearchFilter: ldapConfig.groupSearchFilter,
caCert
};
};
@ -305,7 +351,7 @@ export const ldapConfigServiceFactory = ({
bindCredentials: ldapConfig.bindPass,
searchBase: ldapConfig.searchBase,
searchFilter: "(uid={{username}})",
searchAttributes: ["uid", "uidNumber", "givenName", "sn", "mail"],
// searchAttributes: ["uid", "uidNumber", "givenName", "sn", "mail"],
...(ldapConfig.caCert !== ""
? {
tlsOptions: {
@ -320,7 +366,17 @@ export const ldapConfigServiceFactory = ({
return { opts, ldapConfig };
};
const ldapLogin = async ({ externalId, username, firstName, lastName, emails, orgId, relayState }: TLdapLoginDTO) => {
const ldapLogin = async ({
ldapConfigId,
externalId,
username,
firstName,
lastName,
emails,
groups,
orgId,
relayState
}: TLdapLoginDTO) => {
const appCfg = getConfig();
let userAlias = await userAliasDAL.findOne({
externalId,
@ -394,7 +450,84 @@ export const ldapConfigServiceFactory = ({
});
}
const user = await userDAL.findOne({ id: userAlias.userId });
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 isUserCompleted = Boolean(user.isAccepted);
@ -424,6 +557,99 @@ 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;
};
return {
createLdapCfg,
updateLdapCfg,
@ -431,6 +657,9 @@ export const ldapConfigServiceFactory = ({
getLdapCfg,
// getLdapPassportOpts,
ldapLogin,
bootLdap
bootLdap,
getLdapGroupMaps,
createLdapGroupMap,
deleteLdapGroupMap
};
};

View File

@ -1,5 +1,18 @@
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;
@ -7,6 +20,8 @@ export type TCreateLdapCfgDTO = {
bindDN: string;
bindPass: string;
searchBase: string;
groupSearchBase: string;
groupSearchFilter: string;
caCert: string;
} & TOrgPermission;
@ -18,6 +33,8 @@ export type TUpdateLdapCfgDTO = {
bindDN: string;
bindPass: string;
searchBase: string;
groupSearchBase: string;
groupSearchFilter: string;
caCert: string;
}> &
TOrgPermission;
@ -27,11 +44,31 @@ 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;

View File

@ -0,0 +1,72 @@
import ldapjs from "ldapjs";
import { logger } from "@app/lib/logger";
import { TLDAPConfig } from "./ldap-config-types";
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((unbindError) => {
if (unbindError) {
logger.error("Error unbinding LDAP client:", unbindError);
}
});
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((unbindError) => {
if (unbindError) {
logger.error("Error unbinding LDAP client:", unbindError);
}
});
reject(error);
});
res.on("end", () => {
ldapClient.unbind((unbindError) => {
if (unbindError) {
logger.error("Error unbinding LDAP client:", unbindError);
}
});
resolve(groups);
});
}
);
});
};

View File

@ -0,0 +1,41 @@
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 };
};

View File

@ -4,15 +4,20 @@ import jwt from "jsonwebtoken";
import { OrgMembershipRole, OrgMembershipStatus, TableName, TGroups } 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 { TScimDALFactory } from "@app/ee/services/scim/scim-dal";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, ScimRequestError, UnauthorizedError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { TOrgPermission } from "@app/lib/types";
import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type";
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
import { TOrgDALFactory } from "@app/services/org/org-dal";
import { deleteOrgMembership } from "@app/services/org/org-fns";
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 { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
import { TUserDALFactory } from "@app/services/user/user-dal";
@ -42,14 +47,21 @@ import {
type TScimServiceFactoryDep = {
scimDAL: Pick<TScimDALFactory, "create" | "find" | "findById" | "deleteById">;
userDAL: Pick<TUserDALFactory, "findOne" | "create" | "transaction">;
userDAL: Pick<TUserDALFactory, "find" | "findOne" | "create" | "transaction" | "findUserEncKeyByUserIdsBatch">;
orgDAL: Pick<
TOrgDALFactory,
"createMembership" | "findById" | "findMembership" | "deleteMembershipById" | "transaction"
>;
projectDAL: Pick<TProjectDALFactory, "find">;
projectDAL: Pick<TProjectDALFactory, "find" | "findProjectGhostUser">;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find" | "delete">;
groupDAL: Pick<TGroupDALFactory, "create" | "findOne" | "findAllGroupMembers" | "update" | "delete" | "findGroups">;
groupDAL: Pick<
TGroupDALFactory,
"create" | "findOne" | "findAllGroupMembers" | "update" | "delete" | "findGroups" | "transaction"
>;
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
userGroupMembershipDAL: TUserGroupMembershipDALFactory; // TODO: Pick
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany" | "delete">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
smtpService: TSmtpService;
@ -65,6 +77,10 @@ export const scimServiceFactory = ({
projectDAL,
projectMembershipDAL,
groupDAL,
groupProjectDAL,
userGroupMembershipDAL,
projectKeyDAL,
projectBotDAL,
permissionService,
smtpService
}: TScimServiceFactoryDep) => {
@ -473,7 +489,19 @@ export const scimServiceFactory = ({
};
const listScimGroups = async ({ orgId, offset, limit }: TListScimGroupsDTO) => {
const plan = await licenseService.getPlan(orgId);
if (!plan.groups)
throw new BadRequestError({
message: "Failed to list SCIM groups due to plan restriction. Upgrade plan to list SCIM groups."
});
const org = await orgDAL.findById(orgId);
if (!org) {
throw new ScimRequestError({
detail: "Organization Not Found",
status: 404
});
}
if (!org.scimEnabled)
throw new ScimRequestError({
@ -500,30 +528,76 @@ export const scimServiceFactory = ({
});
};
const createScimGroup = async ({ displayName, orgId }: TCreateScimGroupDTO) => {
const createScimGroup = async ({ displayName, orgId, members }: TCreateScimGroupDTO) => {
const plan = await licenseService.getPlan(orgId);
if (!plan.groups)
throw new BadRequestError({
message: "Failed to create a SCIM group due to plan restriction. Upgrade plan to create a SCIM group."
});
const org = await orgDAL.findById(orgId);
if (!org) {
throw new ScimRequestError({
detail: "Organization Not Found",
status: 404
});
}
if (!org.scimEnabled)
throw new ScimRequestError({
detail: "SCIM is disabled for the organization",
status: 403
});
const group = await groupDAL.create({
name: displayName,
slug: slugify(`${displayName}-${alphaNumericNanoId(4)}`),
orgId,
role: OrgMembershipRole.NoAccess
const newGroup = await groupDAL.transaction(async (tx) => {
const group = await groupDAL.create(
{
name: displayName,
slug: slugify(`${displayName}-${alphaNumericNanoId(4)}`),
orgId,
role: OrgMembershipRole.NoAccess
},
tx
);
if (members && members.length) {
const newMembers = await addUsersToGroupByUserIds({
group,
userIds: members.map((member) => member.value),
userDAL,
userGroupMembershipDAL,
orgDAL,
groupProjectDAL,
projectKeyDAL,
projectDAL,
projectBotDAL,
tx
});
return { group, newMembers };
}
return { group, newMembers: [] };
});
return buildScimGroup({
groupId: group.id,
name: group.name,
members: []
groupId: newGroup.group.id,
name: newGroup.group.name,
members: newGroup.newMembers.map((member) => ({
value: member.id,
display: `${member.firstName} ${member.lastName}`
}))
});
};
const getScimGroup = async ({ groupId, orgId }: TGetScimGroupDTO) => {
const plan = await licenseService.getPlan(orgId);
if (!plan.groups)
throw new BadRequestError({
message: "Failed to get SCIM group due to plan restriction. Upgrade plan to get SCIM group."
});
const group = await groupDAL.findOne({
id: groupId,
orgId
@ -553,35 +627,123 @@ export const scimServiceFactory = ({
});
};
const updateScimGroupNamePut = async ({ groupId, orgId, displayName }: TUpdateScimGroupNamePutDTO) => {
const [group] = await groupDAL.update(
{
id: groupId,
orgId
},
{
name: displayName
}
);
const updateScimGroupNamePut = async ({ groupId, orgId, displayName, members }: TUpdateScimGroupNamePutDTO) => {
const plan = await licenseService.getPlan(orgId);
if (!plan.groups)
throw new BadRequestError({
message: "Failed to update SCIM group due to plan restriction. Upgrade plan to update SCIM group."
});
if (!group) {
const org = await orgDAL.findById(orgId);
if (!org) {
throw new ScimRequestError({
detail: "Group Not Found",
detail: "Organization Not Found",
status: 404
});
}
if (!org.scimEnabled)
throw new ScimRequestError({
detail: "SCIM is disabled for the organization",
status: 403
});
const updatedGroup = await groupDAL.transaction(async (tx) => {
const [group] = await groupDAL.update(
{
id: groupId,
orgId
},
{
name: displayName
}
);
if (!group) {
throw new ScimRequestError({
detail: "Group Not Found",
status: 404
});
}
if (members) {
const membersIdsSet = new Set(members.map((member) => member.value));
const directMemberUserIds = (
await userGroupMembershipDAL.find({
groupId: group.id,
isPending: false
})
).map((membership) => membership.userId);
const pendingGroupAdditionsUserIds = (
await userGroupMembershipDAL.find({
groupId: group.id,
isPending: true
})
).map((pendingGroupAddition) => pendingGroupAddition.userId);
const allMembersUserIds = directMemberUserIds.concat(pendingGroupAdditionsUserIds);
const allMembersUserIdsSet = new Set(allMembersUserIds);
const toAddUserIds = members.filter((member) => !allMembersUserIdsSet.has(member.value));
const toRemoveUserIds = allMembersUserIds.filter((userId) => !membersIdsSet.has(userId));
if (toAddUserIds.length) {
await addUsersToGroupByUserIds({
group,
userIds: toAddUserIds.map((member) => member.value),
userDAL,
userGroupMembershipDAL,
orgDAL,
groupProjectDAL,
projectKeyDAL,
projectDAL,
projectBotDAL,
tx
});
}
if (toRemoveUserIds.length) {
await removeUsersFromGroupByUserIds({
group,
userIds: toRemoveUserIds,
userDAL,
userGroupMembershipDAL,
groupProjectDAL,
projectKeyDAL,
tx
});
}
}
return group;
});
return buildScimGroup({
groupId: group.id,
name: group.name,
members: []
groupId: updatedGroup.id,
name: updatedGroup.name,
members
});
};
// TODO: add support for add/remove op
const updateScimGroupNamePatch = async ({ groupId, orgId, operations }: TUpdateScimGroupNamePatchDTO) => {
const plan = await licenseService.getPlan(orgId);
if (!plan.groups)
throw new BadRequestError({
message: "Failed to update SCIM group due to plan restriction. Upgrade plan to update SCIM group."
});
const org = await orgDAL.findById(orgId);
if (!org) {
throw new ScimRequestError({
detail: "Organization Not Found",
status: 404
});
}
if (!org.scimEnabled)
throw new ScimRequestError({
detail: "SCIM is disabled for the organization",
@ -635,6 +797,26 @@ export const scimServiceFactory = ({
};
const deleteScimGroup = async ({ groupId, orgId }: TDeleteScimGroupDTO) => {
const plan = await licenseService.getPlan(orgId);
if (!plan.groups)
throw new BadRequestError({
message: "Failed to delete SCIM group due to plan restriction. Upgrade plan to delete SCIM group."
});
const org = await orgDAL.findById(orgId);
if (!org) {
throw new ScimRequestError({
detail: "Organization Not Found",
status: 404
});
}
if (!org.scimEnabled)
throw new ScimRequestError({
detail: "SCIM is disabled for the organization",
status: 403
});
const [group] = await groupDAL.delete({
id: groupId,
orgId

View File

@ -81,6 +81,11 @@ export type TListScimGroups = {
export type TCreateScimGroupDTO = {
displayName: string;
orgId: string;
members?: {
// TODO: account for members with value and display (is this optional?)
value: string;
display: string;
}[];
};
export type TGetScimGroupDTO = {
@ -92,6 +97,10 @@ export type TUpdateScimGroupNamePutDTO = {
groupId: string;
orgId: string;
displayName: string;
members: {
value: string;
display: string;
}[];
};
export type TUpdateScimGroupNamePatchDTO = {

View File

@ -495,7 +495,11 @@ export const secretApprovalRequestServiceFactory = ({
await projectDAL.checkProjectUpgradeStatus(projectId);
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
if (!folder) throw new BadRequestError({ message: "Folder not found", name: "GenSecretApproval" });
if (!folder)
throw new BadRequestError({
message: "Folder not found for the given environment slug & secret path",
name: "GenSecretApproval"
});
const folderId = folder.id;
const blindIndexCfg = await secretBlindIndexDAL.findOne({ projectId });

View File

@ -282,6 +282,7 @@ export const RAW_SECRETS = {
},
CREATE: {
secretName: "The name of the secret to create.",
projectSlug: "The slug of the project to create the secret in.",
environment: "The slug of the environment to create the secret in.",
secretComment: "Attach a comment to the secret.",
secretPath: "The path to create the secret in.",
@ -301,11 +302,13 @@ export const RAW_SECRETS = {
},
UPDATE: {
secretName: "The name of the secret to update.",
secretComment: "Update comment to the secret.",
environment: "The slug of the environment where the secret is located.",
secretPath: "The path of the secret to update",
secretValue: "The new value of the secret.",
skipMultilineEncoding: "Skip multiline encoding for the secret value.",
type: "The type of the secret to update.",
projectSlug: "The slug of the project to update the secret in.",
workspaceId: "The ID of the project to update the secret in."
},
DELETE: {
@ -313,6 +316,7 @@ export const RAW_SECRETS = {
environment: "The slug of the environment where the secret is located.",
secretPath: "The path of the secret.",
type: "The type of the secret to delete.",
projectSlug: "The slug of the project to delete the secret in.",
workspaceId: "The ID of the project where the secret is located."
}
} as const;

View File

@ -18,6 +18,7 @@ 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";
@ -200,6 +201,7 @@ 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);
@ -290,14 +292,25 @@ export const registerRoutes = async (
projectDAL,
projectMembershipDAL,
groupDAL,
groupProjectDAL,
userGroupMembershipDAL,
projectKeyDAL,
projectBotDAL,
permissionService,
smtpService
});
const ldapService = ldapConfigServiceFactory({
ldapConfigDAL,
ldapGroupMapDAL,
orgDAL,
orgBotDAL,
groupDAL,
groupProjectDAL,
projectKeyDAL,
projectDAL,
projectBotDAL,
userGroupMembershipDAL,
userDAL,
userAliasDAL,
permissionService,
@ -344,6 +357,11 @@ export const registerRoutes = async (
smtpService,
authDAL,
userDAL,
userGroupMembershipDAL,
projectKeyDAL,
projectDAL,
projectBotDAL,
groupProjectDAL,
orgDAL,
orgService,
licenseService

View File

@ -44,7 +44,6 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
if (req.auth.actor !== ActorType.USER) return;
const users = await server.services.org.findAllOrgMembers(
req.permission.id,
req.params.organizationId,

View File

@ -1656,4 +1656,263 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
return { secrets };
}
});
server.route({
method: "POST",
url: "/batch/raw",
config: {
rateLimit: secretsLimit
},
schema: {
description: "Create many secrets",
security: [
{
bearerAuth: []
}
],
body: z.object({
projectSlug: z.string().trim().describe(RAW_SECRETS.CREATE.projectSlug),
environment: z.string().trim().describe(RAW_SECRETS.CREATE.environment),
secretPath: z
.string()
.trim()
.default("/")
.transform(removeTrailingSlash)
.describe(RAW_SECRETS.CREATE.secretPath),
secrets: z
.object({
secretKey: z.string().trim().describe(RAW_SECRETS.CREATE.secretName),
secretValue: z
.string()
.transform((val) => (val.at(-1) === "\n" ? `${val.trim()}\n` : val.trim()))
.describe(RAW_SECRETS.CREATE.secretValue),
secretComment: z.string().trim().optional().default("").describe(RAW_SECRETS.CREATE.secretComment),
skipMultilineEncoding: z.boolean().optional().describe(RAW_SECRETS.CREATE.skipMultilineEncoding)
})
.array()
.min(1)
}),
response: {
200: z.object({
secrets: secretRawSchema.array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { environment, projectSlug, secretPath, secrets: inputSecrets } = req.body;
const secrets = await server.services.secret.createManySecretsRaw({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
secretPath,
environment,
projectSlug,
secrets: inputSecrets
});
await server.services.auditLog.createAuditLog({
projectId: secrets[0].workspace,
...req.auditLogInfo,
event: {
type: EventType.CREATE_SECRETS,
metadata: {
environment: req.body.environment,
secretPath: req.body.secretPath,
secrets: secrets.map((secret, i) => ({
secretId: secret.id,
secretKey: inputSecrets[i].secretKey,
secretVersion: secret.version
}))
}
}
});
await server.services.telemetry.sendPostHogEvents({
event: PostHogEventTypes.SecretCreated,
distinctId: getTelemetryDistinctId(req),
properties: {
numberOfSecrets: secrets.length,
workspaceId: secrets[0].workspace,
environment: req.body.environment,
secretPath: req.body.secretPath,
channel: getUserAgentType(req.headers["user-agent"]),
...req.auditLogInfo
}
});
return { secrets };
}
});
server.route({
method: "PATCH",
url: "/batch/raw",
config: {
rateLimit: secretsLimit
},
schema: {
description: "Update many secrets",
security: [
{
bearerAuth: []
}
],
body: z.object({
projectSlug: z.string().trim().describe(RAW_SECRETS.UPDATE.projectSlug),
environment: z.string().trim().describe(RAW_SECRETS.UPDATE.environment),
secretPath: z
.string()
.trim()
.default("/")
.transform(removeTrailingSlash)
.describe(RAW_SECRETS.UPDATE.secretPath),
secrets: z
.object({
secretKey: z.string().trim().describe(RAW_SECRETS.UPDATE.secretName),
secretValue: z
.string()
.transform((val) => (val.at(-1) === "\n" ? `${val.trim()}\n` : val.trim()))
.describe(RAW_SECRETS.UPDATE.secretValue),
secretComment: z.string().trim().optional().describe(RAW_SECRETS.UPDATE.secretComment),
skipMultilineEncoding: z.boolean().optional().describe(RAW_SECRETS.UPDATE.skipMultilineEncoding)
})
.array()
.min(1)
}),
response: {
200: z.object({
secrets: secretRawSchema.array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { environment, projectSlug, secretPath, secrets: inputSecrets } = req.body;
const secrets = await server.services.secret.updateManySecretsRaw({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
secretPath,
environment,
projectSlug,
secrets: inputSecrets
});
await server.services.auditLog.createAuditLog({
projectId: secrets[0].workspace,
...req.auditLogInfo,
event: {
type: EventType.UPDATE_SECRETS,
metadata: {
environment: req.body.environment,
secretPath: req.body.secretPath,
secrets: secrets.map((secret, i) => ({
secretId: secret.id,
secretKey: inputSecrets[i].secretKey,
secretVersion: secret.version
}))
}
}
});
await server.services.telemetry.sendPostHogEvents({
event: PostHogEventTypes.SecretUpdated,
distinctId: getTelemetryDistinctId(req),
properties: {
numberOfSecrets: secrets.length,
workspaceId: secrets[0].workspace,
environment: req.body.environment,
secretPath: req.body.secretPath,
channel: getUserAgentType(req.headers["user-agent"]),
...req.auditLogInfo
}
});
return { secrets };
}
});
server.route({
method: "DELETE",
url: "/batch/raw",
config: {
rateLimit: secretsLimit
},
schema: {
description: "Delete many secrets",
security: [
{
bearerAuth: []
}
],
body: z.object({
projectSlug: z.string().trim().describe(RAW_SECRETS.DELETE.projectSlug),
environment: z.string().trim().describe(RAW_SECRETS.DELETE.environment),
secretPath: z
.string()
.trim()
.default("/")
.transform(removeTrailingSlash)
.describe(RAW_SECRETS.DELETE.secretPath),
secrets: z
.object({
secretKey: z.string().trim().describe(RAW_SECRETS.DELETE.secretName)
})
.array()
.min(1)
}),
response: {
200: z.object({
secrets: secretRawSchema.array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { environment, projectSlug, secretPath, secrets: inputSecrets } = req.body;
const secrets = await server.services.secret.deleteManySecretsRaw({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
environment,
projectSlug,
secretPath,
secrets: inputSecrets
});
await server.services.auditLog.createAuditLog({
projectId: secrets[0].workspace,
...req.auditLogInfo,
event: {
type: EventType.DELETE_SECRETS,
metadata: {
environment: req.body.environment,
secretPath: req.body.secretPath,
secrets: secrets.map((secret, i) => ({
secretId: secret.id,
secretKey: inputSecrets[i].secretKey,
secretVersion: secret.version
}))
}
}
});
await server.services.telemetry.sendPostHogEvents({
event: PostHogEventTypes.SecretDeleted,
distinctId: getTelemetryDistinctId(req),
properties: {
numberOfSecrets: secrets.length,
workspaceId: secrets[0].workspace,
environment: req.body.environment,
secretPath: req.body.secretPath,
channel: getUserAgentType(req.headers["user-agent"]),
...req.auditLogInfo
}
});
return { secrets };
}
});
};

View File

@ -191,7 +191,7 @@ export const authLoginServiceFactory = ({
const decodedProviderToken = validateProviderAuthToken(providerAuthToken, email);
authMethod = decodedProviderToken.authMethod;
if (isAuthMethodSaml(authMethod) && decodedProviderToken.orgId) {
if ((isAuthMethodSaml(authMethod) || authMethod === AuthMethod.LDAP) && decodedProviderToken.orgId) {
organizationId = decodedProviderToken.orgId;
}
}

View File

@ -1,10 +1,16 @@
import jwt from "jsonwebtoken";
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 { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors";
import { isDisposableEmail } from "@app/lib/validator";
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-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 { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
import { TokenType } from "../auth-token/auth-token-types";
@ -20,6 +26,14 @@ import { AuthMethod, AuthTokenType } from "./auth-type";
type TAuthSignupDep = {
authDAL: TAuthDALFactory;
userDAL: TUserDALFactory;
userGroupMembershipDAL: Pick<
TUserGroupMembershipDALFactory,
"find" | "transaction" | "insertMany" | "deletePendingUserGroupMembershipsByUserIds"
>;
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany">;
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
orgService: Pick<TOrgServiceFactory, "createOrganization">;
orgDAL: TOrgDALFactory;
tokenService: TAuthTokenServiceFactory;
@ -31,6 +45,11 @@ export type TAuthSignupFactory = ReturnType<typeof authSignupServiceFactory>;
export const authSignupServiceFactory = ({
authDAL,
userDAL,
userGroupMembershipDAL,
projectKeyDAL,
projectDAL,
projectBotDAL,
groupProjectDAL,
tokenService,
smtpService,
orgService,
@ -168,6 +187,16 @@ export const authSignupServiceFactory = ({
const uniqueOrgId = [...new Set(updatedMembersips.map(({ orgId }) => orgId))];
await Promise.allSettled(uniqueOrgId.map((orgId) => licenseService.updateSubscriptionOrgMemberCount(orgId)));
await convertPendingGroupAdditionsToGroupMemberships({
userIds: [user.id],
userDAL,
userGroupMembershipDAL,
groupProjectDAL,
projectKeyDAL,
projectDAL,
projectBotDAL
});
const tokenSession = await tokenService.getUserTokenSession({
userAgent,
ip,
@ -270,6 +299,17 @@ export const authSignupServiceFactory = ({
const uniqueOrgId = [...new Set(updatedMembersips.map(({ orgId }) => orgId))];
await Promise.allSettled(uniqueOrgId.map((orgId) => licenseService.updateSubscriptionOrgMemberCount(orgId)));
await convertPendingGroupAdditionsToGroupMemberships({
userIds: [user.id],
userDAL,
userGroupMembershipDAL,
groupProjectDAL,
projectKeyDAL,
projectDAL,
projectBotDAL,
tx
});
return { info: us, key: userEncKey };
});

View File

@ -32,7 +32,7 @@ type TGroupProjectServiceFactoryDep = {
TGroupProjectMembershipRoleDALFactory,
"create" | "transaction" | "insertMany" | "delete"
>;
userGroupMembershipDAL: TUserGroupMembershipDALFactory;
userGroupMembershipDAL: Pick<TUserGroupMembershipDALFactory, "findGroupMembersNotInProject">;
projectDAL: Pick<TProjectDALFactory, "findOne" | "findProjectGhostUser">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "findLatestProjectKey" | "delete" | "insertMany" | "transaction">;
projectRoleDAL: Pick<TProjectRoleDALFactory, "find">;
@ -116,68 +116,69 @@ export const groupProjectServiceFactory = ({
},
tx
);
// share project key with users in group that have not
// individually been added to the project and that are not part of
// other groups that are in the project
const groupMembers = await userGroupMembershipDAL.findGroupMembersNotInProject(group.id, project.id, tx);
if (groupMembers.length) {
const ghostUser = await projectDAL.findProjectGhostUser(project.id, tx);
if (!ghostUser) {
throw new BadRequestError({
message: "Failed to find sudo user"
});
}
const ghostUserLatestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.id, project.id, tx);
if (!ghostUserLatestKey) {
throw new BadRequestError({
message: "Failed to find sudo user latest key"
});
}
const bot = await projectBotDAL.findOne({ projectId: project.id }, tx);
if (!bot) {
throw new BadRequestError({
message: "Failed to find bot"
});
}
const botPrivateKey = infisicalSymmetricDecrypt({
keyEncoding: bot.keyEncoding as SecretKeyEncoding,
iv: bot.iv,
tag: bot.tag,
ciphertext: bot.encryptedPrivateKey
});
const plaintextProjectKey = decryptAsymmetric({
ciphertext: ghostUserLatestKey.encryptedKey,
nonce: ghostUserLatestKey.nonce,
publicKey: ghostUserLatestKey.sender.publicKey,
privateKey: botPrivateKey
});
const projectKeyData = groupMembers.map(({ user: { publicKey, id } }) => {
const { ciphertext: encryptedKey, nonce } = encryptAsymmetric(plaintextProjectKey, publicKey, botPrivateKey);
return {
encryptedKey,
nonce,
senderId: ghostUser.id,
receiverId: id,
projectId: project.id
};
});
await projectKeyDAL.insertMany(projectKeyData, tx);
}
return groupProjectMembership;
});
// share project key with users in group that have not
// individually been added to the project and that are not part of
// other groups that are in the project
const groupMembers = await userGroupMembershipDAL.findGroupMembersNotInProject(group.id, project.id);
if (groupMembers.length) {
const ghostUser = await projectDAL.findProjectGhostUser(project.id);
if (!ghostUser) {
throw new BadRequestError({
message: "Failed to find sudo user"
});
}
const ghostUserLatestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.id, project.id);
if (!ghostUserLatestKey) {
throw new BadRequestError({
message: "Failed to find sudo user latest key"
});
}
const bot = await projectBotDAL.findOne({ projectId: project.id });
if (!bot) {
throw new BadRequestError({
message: "Failed to find bot"
});
}
const botPrivateKey = infisicalSymmetricDecrypt({
keyEncoding: bot.keyEncoding as SecretKeyEncoding,
iv: bot.iv,
tag: bot.tag,
ciphertext: bot.encryptedPrivateKey
});
const plaintextProjectKey = decryptAsymmetric({
ciphertext: ghostUserLatestKey.encryptedKey,
nonce: ghostUserLatestKey.nonce,
publicKey: ghostUserLatestKey.sender.publicKey,
privateKey: botPrivateKey
});
const projectKeyData = groupMembers.map(({ user: { publicKey, id } }) => {
const { ciphertext: encryptedKey, nonce } = encryptAsymmetric(plaintextProjectKey, publicKey, botPrivateKey);
return {
encryptedKey,
nonce,
senderId: ghostUser.id,
receiverId: id,
projectId: project.id
};
});
await projectKeyDAL.insertMany(projectKeyData);
}
return projectGroup;
};
@ -287,20 +288,26 @@ export const groupProjectServiceFactory = ({
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Groups);
const groupMembers = await userGroupMembershipDAL.findGroupMembersNotInProject(group.id, project.id);
const deletedProjectGroup = await groupProjectDAL.transaction(async (tx) => {
const groupMembers = await userGroupMembershipDAL.findGroupMembersNotInProject(group.id, project.id, tx);
if (groupMembers.length) {
await projectKeyDAL.delete({
projectId: project.id,
$in: {
receiverId: groupMembers.map(({ user: { id } }) => id)
}
});
}
if (groupMembers.length) {
await projectKeyDAL.delete(
{
projectId: project.id,
$in: {
receiverId: groupMembers.map(({ user: { id } }) => id)
}
},
tx
);
}
const [deletedGroup] = await groupProjectDAL.delete({ groupId: group.id, projectId: project.id });
const [projectGroup] = await groupProjectDAL.delete({ groupId: group.id, projectId: project.id }, tx);
return projectGroup;
});
return deletedGroup;
return deletedProjectGroup;
};
const listGroupsInProject = async ({

View File

@ -89,6 +89,25 @@ export const orgDALFactory = (db: TDbClient) => {
}
};
const countAllOrgMembers = async (orgId: string) => {
try {
interface CountResult {
count: string;
}
const count = await db(TableName.OrgMembership)
.where(`${TableName.OrgMembership}.orgId`, orgId)
.count("*")
.join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`)
.where({ isGhost: false })
.first();
return parseInt((count as unknown as CountResult).count || "0", 10);
} catch (error) {
throw new DatabaseError({ error, name: "Count all org members" });
}
};
const findOrgMembersByUsername = async (orgId: string, usernames: string[]) => {
try {
const members = await db(TableName.OrgMembership)
@ -269,6 +288,7 @@ export const orgDALFactory = (db: TDbClient) => {
...orgOrm,
findOrgByProjectId,
findAllOrgMembers,
countAllOrgMembers,
findOrgById,
findAllOrgsByUserId,
ghostUserExists,

View File

@ -248,7 +248,7 @@ export const orgServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Scim);
}
if (authEnforced || scimEnabled) {
if (authEnforced) {
const samlCfg = await samlConfigDAL.findEnforceableSamlCfg(orgId);
if (!samlCfg)
throw new BadRequestError({

View File

@ -81,9 +81,9 @@ export const projectDALFactory = (db: TDbClient) => {
}
};
const findProjectGhostUser = async (projectId: string) => {
const findProjectGhostUser = async (projectId: string, tx?: Knex) => {
try {
const ghostUser = await db(TableName.ProjectMembership)
const ghostUser = await (tx || db)(TableName.ProjectMembership)
.where({ projectId })
.join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`)
.select(selectAllTableCols(TableName.Users))

View File

@ -575,7 +575,11 @@ export const createManySecretsRawFnFactory = ({
await projectDAL.checkProjectUpgradeStatus(projectId);
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create secret" });
if (!folder)
throw new BadRequestError({
message: "Folder not found for the given environment slug & secret path",
name: "Create secret"
});
const folderId = folder.id;
const blindIndexCfg = await secretBlindIndexDAL.findOne({ projectId });
@ -680,7 +684,11 @@ export const updateManySecretsRawFnFactory = ({
await projectDAL.checkProjectUpgradeStatus(projectId);
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Update secret" });
if (!folder)
throw new BadRequestError({
message: "Folder not found for the given environment slug & secret path",
name: "Update secret"
});
const folderId = folder.id;
const blindIndexCfg = await secretBlindIndexDAL.findOne({ projectId });

View File

@ -33,9 +33,11 @@ import { TSecretQueueFactory } from "./secret-queue";
import {
TAttachSecretTagsDTO,
TCreateBulkSecretDTO,
TCreateManySecretRawDTO,
TCreateSecretDTO,
TCreateSecretRawDTO,
TDeleteBulkSecretDTO,
TDeleteManySecretRawDTO,
TDeleteSecretDTO,
TDeleteSecretRawDTO,
TFnSecretBlindIndexCheckV2,
@ -46,6 +48,7 @@ import {
TGetSecretsRawDTO,
TGetSecretVersionsDTO,
TUpdateBulkSecretDTO,
TUpdateManySecretRawDTO,
TUpdateSecretDTO,
TUpdateSecretRawDTO
} from "./secret-types";
@ -179,7 +182,11 @@ export const secretServiceFactory = ({
await projectDAL.checkProjectUpgradeStatus(projectId);
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create secret" });
if (!folder)
throw new BadRequestError({
message: "Folder not found for the given environment slug & secret path",
name: "Create secret"
});
const folderId = folder.id;
const blindIndexCfg = await secretBlindIndexDAL.findOne({ projectId });
@ -275,7 +282,11 @@ export const secretServiceFactory = ({
}
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create secret" });
if (!folder)
throw new BadRequestError({
message: "Folder not found for the given environment slug & secret path",
name: "Create secret"
});
const folderId = folder.id;
const blindIndexCfg = await secretBlindIndexDAL.findOne({ projectId });
@ -391,7 +402,11 @@ export const secretServiceFactory = ({
await projectDAL.checkProjectUpgradeStatus(projectId);
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create secret" });
if (!folder)
throw new BadRequestError({
message: "Folder not found for the given environment slug & secret path",
name: "Create secret"
});
const folderId = folder.id;
const blindIndexCfg = await secretBlindIndexDAL.findOne({ projectId });
@ -559,7 +574,11 @@ export const secretServiceFactory = ({
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
);
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create secret" });
if (!folder)
throw new BadRequestError({
message: "Folder not found for the given environment slug & secret path",
name: "Create secret"
});
const folderId = folder.id;
const secretBlindIndex = await interalGenSecBlindIndexByName(projectId, secretName);
@ -655,7 +674,11 @@ export const secretServiceFactory = ({
await projectDAL.checkProjectUpgradeStatus(projectId);
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create secret" });
if (!folder)
throw new BadRequestError({
message: "Folder not found for the given environment slug & secret path",
name: "Create secret"
});
const folderId = folder.id;
const blindIndexCfg = await secretBlindIndexDAL.findOne({ projectId });
@ -724,7 +747,11 @@ export const secretServiceFactory = ({
await projectDAL.checkProjectUpgradeStatus(projectId);
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Update secret" });
if (!folder)
throw new BadRequestError({
message: "Folder not found for the given environment slug & secret path",
name: "Update secret"
});
const folderId = folder.id;
const blindIndexCfg = await secretBlindIndexDAL.findOne({ projectId });
@ -810,7 +837,11 @@ export const secretServiceFactory = ({
await projectDAL.checkProjectUpgradeStatus(projectId);
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create secret" });
if (!folder)
throw new BadRequestError({
message: "Folder not found for the given environment slug & secret path",
name: "Create secret"
});
const folderId = folder.id;
const blindIndexCfg = await secretBlindIndexDAL.findOne({ projectId });
@ -1036,6 +1067,143 @@ export const secretServiceFactory = ({
return decryptSecretRaw(secret, botKey);
};
const createManySecretsRaw = async ({
actorId,
projectSlug,
environment,
actor,
actorOrgId,
actorAuthMethod,
secretPath,
secrets: inputSecrets = []
}: TCreateManySecretRawDTO) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new BadRequestError({ message: "Project not found" });
const projectId = project.id;
const botKey = await projectBotService.getBotKey(projectId);
if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
const secrets = await createManySecret({
projectId,
environment,
path: secretPath,
actor,
actorId,
actorOrgId,
actorAuthMethod,
secrets: inputSecrets.map(({ secretComment, secretKey, secretValue, skipMultilineEncoding }) => {
const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(secretKey, botKey);
const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(secretValue || "", botKey);
const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8(secretComment || "", botKey);
return {
secretName: secretKey,
skipMultilineEncoding,
secretKeyCiphertext: secretKeyEncrypted.ciphertext,
secretKeyIV: secretKeyEncrypted.iv,
secretKeyTag: secretKeyEncrypted.tag,
secretValueCiphertext: secretValueEncrypted.ciphertext,
secretValueIV: secretValueEncrypted.iv,
secretValueTag: secretValueEncrypted.tag,
secretCommentCiphertext: secretCommentEncrypted.ciphertext,
secretCommentIV: secretCommentEncrypted.iv,
secretCommentTag: secretCommentEncrypted.tag
};
})
});
await snapshotService.performSnapshot(secrets[0].folderId);
await secretQueueService.syncSecrets({ secretPath, projectId, environment });
return secrets.map((secret) => decryptSecretRaw({ ...secret, workspace: projectId, environment }, botKey));
};
const updateManySecretsRaw = async ({
actorId,
projectSlug,
environment,
actor,
actorOrgId,
actorAuthMethod,
secretPath,
secrets: inputSecrets = []
}: TUpdateManySecretRawDTO) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new BadRequestError({ message: "Project not found" });
const projectId = project.id;
const botKey = await projectBotService.getBotKey(projectId);
if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
const secrets = await updateManySecret({
projectId,
environment,
path: secretPath,
actor,
actorId,
actorOrgId,
actorAuthMethod,
secrets: inputSecrets.map(({ secretComment, secretKey, secretValue, skipMultilineEncoding }) => {
const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(secretKey, botKey);
const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(secretValue || "", botKey);
const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8(secretComment || "", botKey);
return {
secretName: secretKey,
type: SecretType.Shared,
skipMultilineEncoding,
secretKeyCiphertext: secretKeyEncrypted.ciphertext,
secretKeyIV: secretKeyEncrypted.iv,
secretKeyTag: secretKeyEncrypted.tag,
secretValueCiphertext: secretValueEncrypted.ciphertext,
secretValueIV: secretValueEncrypted.iv,
secretValueTag: secretValueEncrypted.tag,
secretCommentCiphertext: secretCommentEncrypted.ciphertext,
secretCommentIV: secretCommentEncrypted.iv,
secretCommentTag: secretCommentEncrypted.tag
};
})
});
await snapshotService.performSnapshot(secrets[0].folderId);
await secretQueueService.syncSecrets({ secretPath, projectId, environment });
return secrets.map((secret) => decryptSecretRaw({ ...secret, workspace: projectId, environment }, botKey));
};
const deleteManySecretsRaw = async ({
actorId,
projectSlug,
environment,
actor,
actorOrgId,
actorAuthMethod,
secretPath,
secrets: inputSecrets = []
}: TDeleteManySecretRawDTO) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new BadRequestError({ message: "Project not found" });
const projectId = project.id;
const botKey = await projectBotService.getBotKey(projectId);
if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
const secrets = await deleteManySecret({
projectId,
environment,
path: secretPath,
actor,
actorId,
actorOrgId,
actorAuthMethod,
secrets: inputSecrets.map(({ secretKey }) => ({ secretName: secretKey, type: SecretType.Shared }))
});
await snapshotService.performSnapshot(secrets[0].folderId);
await secretQueueService.syncSecrets({ secretPath, projectId, environment });
return secrets.map((secret) => decryptSecretRaw({ ...secret, workspace: projectId, environment }, botKey));
};
const getSecretVersions = async ({
actorId,
actor,
@ -1280,6 +1448,9 @@ export const secretServiceFactory = ({
createSecretRaw,
updateSecretRaw,
deleteSecretRaw,
createManySecretsRaw,
updateManySecretsRaw,
deleteManySecretsRaw,
getSecretVersions,
// external services function
fnSecretBulkDelete,

View File

@ -181,6 +181,39 @@ export type TDeleteSecretRawDTO = TProjectPermission & {
type: SecretType;
};
export type TCreateManySecretRawDTO = Omit<TProjectPermission, "projectId"> & {
secretPath: string;
projectSlug: string;
environment: string;
secrets: {
secretKey: string;
secretValue: string;
secretComment?: string;
skipMultilineEncoding?: boolean;
}[];
};
export type TUpdateManySecretRawDTO = Omit<TProjectPermission, "projectId"> & {
secretPath: string;
projectSlug: string;
environment: string;
secrets: {
secretKey: string;
secretValue: string;
secretComment?: string;
skipMultilineEncoding?: boolean;
}[];
};
export type TDeleteManySecretRawDTO = Omit<TProjectPermission, "projectId"> & {
secretPath: string;
projectSlug: string;
environment: string;
secrets: {
secretKey: string;
}[];
};
export type TGetSecretVersionsDTO = Omit<TProjectPermission, "projectId"> & {
limit?: number;
offset?: number;

View File

@ -34,6 +34,19 @@ export const userDALFactory = (db: TDbClient) => {
}
};
const findUserEncKeyByUserIdsBatch = async ({ userIds }: { userIds: string[] }, tx?: Knex) => {
try {
return await (tx || db)(TableName.Users)
.where({
isGhost: false
})
.whereIn(`${TableName.Users}.id`, userIds)
.join(TableName.UserEncryptionKey, `${TableName.Users}.id`, `${TableName.UserEncryptionKey}.userId`);
} catch (error) {
throw new DatabaseError({ error, name: "Find user enc by user ids batch" });
}
};
const findUserEncKeyByUserId = async (userId: string) => {
try {
const user = await db(TableName.Users)
@ -123,6 +136,7 @@ export const userDALFactory = (db: TDbClient) => {
...userOrm,
findUserByUsername,
findUserEncKeyByUsername,
findUserEncKeyByUserIdsBatch,
findUserEncKeyByUserId,
updateUserEncryptionByUserId,
findUserByProjectMembershipId,

View File

@ -22,10 +22,6 @@ 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")

View File

@ -7,6 +7,7 @@ import (
"crypto/sha256"
"encoding/base64"
"fmt"
"os"
"regexp"
"sort"
"strings"
@ -204,8 +205,10 @@ 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}, "")
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, SecretsPath: secretsPath, InfisicalToken: infisicalTokenEnv}, "")
if err != nil {
util.HandleError(err, "unable to retrieve secrets")
}

View File

@ -2,7 +2,6 @@ package util
import (
"fmt"
"os"
"strings"
"github.com/Infisical/infisical-merge/packages/api"
@ -13,13 +12,11 @@ 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")

View File

@ -307,10 +307,6 @@ 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

View File

@ -0,0 +1,8 @@
---
title: "Bulk Create"
openapi: "POST /api/v3/secrets/batch/raw"
---
<Tip>
This endpoint requires you to disable end-to-end encryption. For more information, you should consult this [note](https://infisical.com/docs/api-reference/overview/examples/note).
</Tip>

View File

@ -0,0 +1,8 @@
---
title: "Bulk Delete"
openapi: "DELETE /api/v3/secrets/batch/raw"
---
<Tip>
This endpoint requires you to disable end-to-end encryption. For more information, you should consult this [note](https://infisical.com/docs/api-reference/overview/examples/note).
</Tip>

View File

@ -0,0 +1,8 @@
---
title: "Bulk Update"
openapi: "PATCH /api/v3/secrets/batch/raw"
---
<Tip>
This endpoint requires you to disable end-to-end encryption. For more information, you should consult this [note](https://infisical.com/docs/api-reference/overview/examples/note).
</Tip>

View File

@ -1,36 +0,0 @@
---
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.
![LDAP configuration](/images/platform/ldap/ldap-config.png)
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.
![LDAP toggle](/images/platform/ldap/ldap-toggle.png)
</Step>
</Steps>

View File

@ -4,16 +4,17 @@ 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 > Authentication > LDAP Configuration and select **Set up LDAP**.
In Infisical, head to your Organization Settings > Security > LDAP and select **Manage**.
Next, input your LDAP server settings.
@ -24,11 +25,41 @@ You can configure your organization in Infisical to have members authenticate wi
- URL: The LDAP server to connect to such as `ldap://ldap.your-org.com`, `ldaps://ldap.myorg.com:636` (for connection over SSL/TLS), etc.
- Bind DN: The distinguished name of object to bind when performing the user search such as `cn=infisical,ou=Users,dc=acme,dc=com`.
- Bind Pass: The password to use along with `Bind DN` when performing the user search.
- Search Base / User DN: Base DN under which to perform user search such as `ou=Users,dc=example,dc=com`
- Search Base / User DN: Base DN under which to perform user search such as `ou=Users,dc=acme,dc=com`
- Group Search Base / Group DN (optional): LDAP search base to use for group membership search such as `ou=Groups,dc=acme,dc=com`.
- Group Filter (optional): Template used when constructing the group membership query such as `(objectClass=posixGroup)`. The template can access the following context variables: [`UserDN`, `UserUID`, `UserName`]. The default is `(|(memberUid={{.Username}})(member={{.UserDN}})(uniqueMember={{.UserDN}}))` which is compatible with several common directory schemas.
- 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="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.
![LDAP group mappings section](/images/platform/ldap/ldap-group-mappings-section.png)
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.
![LDAP group mappings table](/images/platform/ldap/ldap-group-mappings-table.png)
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.
![LDAP toggle](/images/platform/ldap/ldap-toggle.png)
</Step>
</Steps>
</Steps>

View File

@ -4,9 +4,10 @@ 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>
@ -17,13 +18,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**.
![LDAP JumpCloud](/images/platform/ldap/jumpcloud/ldap-jumpcloud-enable-bind-dn.png)
</Step>
<Step title="Prepare the LDAP configuration in Infisical">
In Infisical, head to your Organization Settings > Authentication > LDAP Configuration and select **Set up LDAP**.
In Infisical, head to your Organization Settings > Security > LDAP and select **Manage**.
Next, input your JumpCloud LDAP server settings.
@ -35,20 +36,48 @@ description: "Learn how to configure JumpCloud LDAP for authenticating into Infi
- Bind DN: The distinguished name of object to bind when performing the user search (`uid=<ldap-user-username>,ou=Users,o=<your-org-id>,dc=jumpcloud,dc=com`).
- Bind Pass: The password to use along with `Bind DN` when performing the user search.
- Search Base / User DN: Base DN under which to perform user search (`ou=Users,o=<your-org-id>,dc=jumpcloud,dc=com`).
- 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)`).
- 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="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.
![LDAP group mappings section](/images/platform/ldap/ldap-group-mappings-section.png)
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.
![LDAP group mappings table](/images/platform/ldap/ldap-group-mappings-table.png)
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.
![LDAP toggle](/images/platform/ldap/ldap-toggle.png)
</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)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 427 KiB

After

Width:  |  Height:  |  Size: 439 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 456 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 537 KiB

After

Width:  |  Height:  |  Size: 599 KiB

View File

@ -33,7 +33,7 @@ This approach enables you to fetch secrets from Infisical during Amplify build t
preBuild:
commands:
- sudo curl -1sLf 'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.rpm.sh' | sudo -E bash
- sudo yum install infisical
- sudo yum -y install infisical
```
</Step>
<Step title="Modify the build command">

View File

@ -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 [here](https://hub.docker.com/r/infisical/infisical/tags).
Choose the latest Infisical docker image tag from [here](https://hub.docker.com/r/infisical/infisical/tags).
```yaml values.yaml

View File

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

View File

@ -0,0 +1,138 @@
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,
groupSearchBase,
groupSearchFilter,
caCert
}: {
organizationId: string;
isActive: boolean;
url: string;
bindDN: string;
bindPass: string;
searchBase: string;
groupSearchBase: string;
groupSearchFilter: string;
caCert?: string;
}) => {
const { data } = await apiRequest.post("/api/v1/ldap/config", {
organizationId,
isActive,
url,
bindDN,
bindPass,
searchBase,
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,
groupSearchBase,
groupSearchFilter,
caCert
}: {
organizationId: string;
isActive?: boolean;
url?: string;
bindDN?: string;
bindPass?: string;
searchBase?: string;
groupSearchBase?: string;
groupSearchFilter?: string;
caCert?: string;
}) => {
const { data } = await apiRequest.patch("/api/v1/ldap/config", {
organizationId,
isActive,
url,
bindDN,
bindPass,
searchBase,
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));
}
});
};

View File

@ -1,9 +1,12 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useQuery } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
const ldapConfigKeys = {
getLDAPConfig: (orgId: string) => [{ orgId }, "organization-ldap"] as const
import { LDAPGroupMap } from "./types";
export const ldapConfigKeys = {
getLDAPConfig: (orgId: string) => [{ orgId }, "organization-ldap"] as const,
getLDAPGroupMaps: (ldapConfigId: string) => [{ ldapConfigId }, "ldap-group-maps"] as const
};
export const useGetLDAPConfig = (organizationId: string) => {
@ -18,78 +21,18 @@ export const useGetLDAPConfig = (organizationId: string) => {
});
};
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
});
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`
);
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,
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));
}
enabled: true
});
};

View File

@ -0,0 +1,10 @@
export type LDAPGroupMap = {
id: string;
ldapConfigId: string;
ldapGroupCN: string;
group: {
id: string;
name: string;
slug: string;
};
};

View File

@ -1,32 +1,27 @@
import { useState } from "react";
import { faMagnifyingGlass,faUsers } from "@fortawesome/free-solid-svg-icons";
import { faMagnifyingGlass, faUsers } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import {
Button,
EmptyState,
Input,
Modal,
ModalContent,
Pagination,
Table,
TableContainer,
TableSkeleton,
TBody,
Td,
Th,
THead,
Tr} from "@app/components/v2";
import {
OrgPermissionActions,
OrgPermissionSubjects
} from "@app/context";
import {
useAddUserToGroup,
useListGroupUsers,
useRemoveUserFromGroup} from "@app/hooks/api";
Button,
EmptyState,
Input,
Modal,
ModalContent,
Pagination,
Table,
TableContainer,
TableSkeleton,
TBody,
Td,
Th,
THead,
Tr
} from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
import { useAddUserToGroup, useListGroupUsers, useRemoveUserFromGroup } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = {
@ -34,136 +29,127 @@ type Props = {
handlePopUpToggle: (popUpName: keyof UsePopUpState<["groupMembers"]>, state?: boolean) => void;
};
export const OrgGroupMembersModal = ({
popUp,
handlePopUpToggle
}: Props) => {
const [page, setPage] = useState(1);
const [perPage, setPerPage] = useState(10);
const [searchMemberFilter, setSearchMemberFilter] = useState("");
const popUpData = popUp?.groupMembers?.data as {
slug: string;
};
const { data, isLoading } = useListGroupUsers({
groupSlug: popUpData?.slug,
offset: (page - 1) * perPage,
limit: perPage,
username: searchMemberFilter
});
const { mutateAsync: assignMutateAsync } = useAddUserToGroup();
const { mutateAsync: unassignMutateAsync } = useRemoveUserFromGroup();
const handleAssignment = async (username: string, assign: boolean) => {
try {
if (!popUpData?.slug) return;
if (assign) {
await assignMutateAsync({
username,
slug: popUpData.slug
});
} else {
await unassignMutateAsync({
username,
slug: popUpData.slug
});
}
export const OrgGroupMembersModal = ({ popUp, handlePopUpToggle }: Props) => {
const [page, setPage] = useState(1);
const [perPage, setPerPage] = useState(10);
const [searchMemberFilter, setSearchMemberFilter] = useState("");
createNotification({
text: `Successfully ${assign ? "assigned" : "removed "} user ${assign ? "to" : "from"} group`,
type: "success"
});
} catch (err) {
createNotification({
text: `Failed to ${assign ? "assigned" : "remove"} user ${assign ? "to" : "from"} group`,
type: "error"
});
}
const popUpData = popUp?.groupMembers?.data as {
slug: string;
};
const { data, isLoading } = useListGroupUsers({
groupSlug: popUpData?.slug,
offset: (page - 1) * perPage,
limit: perPage,
username: searchMemberFilter
});
const { mutateAsync: assignMutateAsync } = useAddUserToGroup();
const { mutateAsync: unassignMutateAsync } = useRemoveUserFromGroup();
const handleAssignment = async (username: string, assign: boolean) => {
try {
if (!popUpData?.slug) return;
if (assign) {
await assignMutateAsync({
username,
slug: popUpData.slug
});
} else {
await unassignMutateAsync({
username,
slug: popUpData.slug
});
}
createNotification({
text: `Successfully ${assign ? "assigned" : "removed"} user ${
assign ? "to" : "from"
} group`,
type: "success"
});
} catch (err) {
createNotification({
text: `Failed to ${assign ? "assign" : "remove"} user ${assign ? "to" : "from"} group`,
type: "error"
});
}
return (
<Modal
isOpen={popUp?.groupMembers?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("groupMembers", isOpen);
}}
>
<ModalContent title="Manage Group Members">
<Input
value={searchMemberFilter}
onChange={(e) => setSearchMemberFilter(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search members..."
/>
<TableContainer className="mt-4">
<Table>
<THead>
<Tr>
<Th>User</Th>
<Th />
</Tr>
</THead>
<TBody>
{isLoading && <TableSkeleton columns={2} innerKey="group-users" />}
{!isLoading && data?.users?.map(({
id,
firstName,
lastName,
username,
isPartOfGroup
}) => {
return (
<Tr className="items-center" key={`group-user-${id}`}>
<Td>
<p>{`${firstName} ${lastName}`}</p>
<p>{username}</p>
</Td>
<Td className="flex justify-end">
<OrgPermissionCan
I={OrgPermissionActions.Edit}
a={OrgPermissionSubjects.Groups}
>
{(isAllowed) => {
return (
<Button
isLoading={isLoading}
isDisabled={!isAllowed}
colorSchema="primary"
variant="outline_bg"
type="submit"
onClick={() => handleAssignment(username, !isPartOfGroup)}
>
{isPartOfGroup ? "Unassign" : "Assign"}
</Button>
);
}}
</OrgPermissionCan>
</Td>
</Tr>
);
})}
</TBody>
</Table>
{!isLoading && data?.totalCount !== undefined && (
<Pagination
count={data.totalCount}
page={page}
perPage={perPage}
onChangePage={(newPage) => setPage(newPage)}
onChangePerPage={(newPerPage) => setPerPage(newPerPage)}
/>
)}
{!isLoading && !data?.users?.length && (
<EmptyState
title="No users found"
icon={faUsers}
/>
)}
</TableContainer>
</ModalContent>
</Modal>
);
}
};
return (
<Modal
isOpen={popUp?.groupMembers?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("groupMembers", isOpen);
}}
>
<ModalContent title="Manage Group Members">
<Input
value={searchMemberFilter}
onChange={(e) => setSearchMemberFilter(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search members..."
/>
<TableContainer className="mt-4">
<Table>
<THead>
<Tr>
<Th>User</Th>
<Th />
</Tr>
</THead>
<TBody>
{isLoading && <TableSkeleton columns={2} innerKey="group-users" />}
{!isLoading &&
data?.users?.map(({ id, firstName, lastName, username, isPartOfGroup }) => {
return (
<Tr className="items-center" key={`group-user-${id}`}>
<Td>
<p>{`${firstName ?? "-"} ${lastName ?? ""}`}</p>
<p>{username}</p>
</Td>
<Td className="flex justify-end">
<OrgPermissionCan
I={OrgPermissionActions.Edit}
a={OrgPermissionSubjects.Groups}
>
{(isAllowed) => {
return (
<Button
isLoading={isLoading}
isDisabled={!isAllowed}
colorSchema="primary"
variant="outline_bg"
type="submit"
onClick={() => handleAssignment(username, !isPartOfGroup)}
>
{isPartOfGroup ? "Unassign" : "Assign"}
</Button>
);
}}
</OrgPermissionCan>
</Td>
</Tr>
);
})}
</TBody>
</Table>
{!isLoading && data?.totalCount !== undefined && (
<Pagination
count={data.totalCount}
page={page}
perPage={perPage}
onChangePage={(newPage) => setPage(newPage)}
onChangePerPage={(newPerPage) => setPerPage(newPerPage)}
/>
)}
{!isLoading && !data?.users?.length && (
<EmptyState title="No users found" icon={faUsers} />
)}
</TableContainer>
</ModalContent>
</Modal>
);
};

View File

@ -19,7 +19,10 @@ import { UsePopUpState } from "@app/hooks/usePopUp";
const GroupFormSchema = z.object({
name: z.string().min(1, "Name cannot be empty").max(50, "Name must be 50 characters or fewer"),
slug: z.string().min(5, "Slug cannot be empty").max(36, "Slug must be 36 characters or fewer"),
slug: z
.string()
.min(5, "Slug must be at least 5 characters long")
.max(36, "Slug must be 36 characters or fewer"),
role: z.string()
});

View File

@ -0,0 +1,256 @@
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>
);
};

View File

@ -14,6 +14,8 @@ const LDAPFormSchema = z.object({
bindDN: z.string().default(""),
bindPass: z.string().default(""),
searchBase: z.string().default(""),
groupSearchBase: z.string().default(""),
groupSearchFilter: z.string().default(""),
caCert: z.string().optional()
});
@ -27,7 +29,7 @@ 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 { data } = useGetLDAPConfig(currentOrg?.id ?? "");
@ -43,12 +45,22 @@ export const LDAPModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props)
bindDN: data?.bindDN ?? "",
bindPass: data?.bindPass ?? "",
searchBase: data?.searchBase ?? "",
groupSearchBase: data?.groupSearchBase ?? "",
groupSearchFilter: data?.groupSearchFilter ?? "",
caCert: data?.caCert ?? ""
});
}
}, [data]);
const onSSOModalSubmit = async ({ url, bindDN, bindPass, searchBase, caCert }: TLDAPFormData) => {
const onSSOModalSubmit = async ({
url,
bindDN,
bindPass,
searchBase,
groupSearchBase,
groupSearchFilter,
caCert
}: TLDAPFormData) => {
try {
if (!currentOrg) return;
@ -60,6 +72,8 @@ export const LDAPModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props)
bindDN,
bindPass,
searchBase,
groupSearchBase,
groupSearchFilter,
caCert
});
} else {
@ -70,6 +84,8 @@ export const LDAPModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props)
bindDN,
bindPass,
searchBase,
groupSearchBase,
groupSearchFilter,
caCert
});
}
@ -139,6 +155,32 @@ export const LDAPModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props)
</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)" />
</FormControl>
)}
/>
<Controller
control={control}
name="caCert"

View File

@ -10,16 +10,20 @@ 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);
@ -63,7 +67,9 @@ export const OrgLDAPSection = (): JSX.Element => {
url: "",
bindDN: "",
bindPass: "",
searchBase: ""
searchBase: "",
groupSearchBase: "",
groupSearchFilter: ""
});
}
@ -76,6 +82,15 @@ export const OrgLDAPSection = (): JSX.Element => {
}
};
const openLDAPGroupMapModal = () => {
if (!subscription?.ldap) {
handlePopUpOpen("upgradePlan");
return;
}
handlePopUpOpen("ldapGroupMap");
};
return (
<>
<hr className="border-mineshaft-600" />
@ -92,6 +107,25 @@ export const OrgLDAPSection = (): JSX.Element => {
</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}
>
Manage
</Button>
)}
</OrgPermissionCan>
</div>
<p className="text-sm text-mineshaft-300">
Manage how LDAP groups are mapped to internal groups in Infisical
</p>
</div>
{data && (
<div className="py-4">
<div className="mb-2 flex items-center justify-between">
@ -119,6 +153,11 @@ 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)}