Merge branch 'heads/main' into overview-page-enchanced

This commit is contained in:
Rhythm Bhiwani
2024-03-12 11:11:20 +05:30
111 changed files with 2177 additions and 747 deletions
+3
View File
@@ -7,6 +7,9 @@ push:
up-dev: up-dev:
docker compose -f docker-compose.dev.yml up --build docker compose -f docker-compose.dev.yml up --build
up-dev-ldap:
docker compose -f docker-compose.dev.yml --profile ldap up --build
up-prod: up-prod:
docker-compose -f docker-compose.prod.yml up --build docker-compose -f docker-compose.prod.yml up --build
+162
View File
@@ -52,6 +52,7 @@
"passport-github": "^1.1.0", "passport-github": "^1.1.0",
"passport-gitlab2": "^5.0.0", "passport-gitlab2": "^5.0.0",
"passport-google-oauth20": "^2.0.0", "passport-google-oauth20": "^2.0.0",
"passport-ldapauth": "^3.0.1",
"pg": "^8.11.3", "pg": "^8.11.3",
"picomatch": "^3.0.1", "picomatch": "^3.0.1",
"pino": "^8.16.2", "pino": "^8.16.2",
@@ -3973,6 +3974,14 @@
"integrity": "sha512-2h3tFvkbHksiNcDiUdcJ08gXWG10fnahp30GJ2Tbt4vd4pfsbfkoKTaTbYykFoppaJ6DL3914nQ3PU1vVIlBRQ==", "integrity": "sha512-2h3tFvkbHksiNcDiUdcJ08gXWG10fnahp30GJ2Tbt4vd4pfsbfkoKTaTbYykFoppaJ6DL3914nQ3PU1vVIlBRQ==",
"dev": true "dev": true
}, },
"node_modules/@types/ldapjs": {
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/@types/ldapjs/-/ldapjs-2.2.5.tgz",
"integrity": "sha512-Lv/nD6QDCmcT+V1vaTRnEKE8UgOilVv5pHcQuzkU1LcRe4mbHHuUo/KHi0LKrpdHhQY8FJzryF38fcVdeUIrzg==",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/libsodium-wrappers": { "node_modules/@types/libsodium-wrappers": {
"version": "0.7.13", "version": "0.7.13",
"resolved": "https://registry.npmjs.org/@types/libsodium-wrappers/-/libsodium-wrappers-0.7.13.tgz", "resolved": "https://registry.npmjs.org/@types/libsodium-wrappers/-/libsodium-wrappers-0.7.13.tgz",
@@ -5120,6 +5129,22 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/asn1": {
"version": "0.2.6",
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz",
"integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==",
"dependencies": {
"safer-buffer": "~2.1.0"
}
},
"node_modules/assert-plus": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
"integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==",
"engines": {
"node": ">=0.8"
}
},
"node_modules/assertion-error": { "node_modules/assertion-error": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz",
@@ -5286,6 +5311,17 @@
"axios": "0.x || 1.x" "axios": "0.x || 1.x"
} }
}, },
"node_modules/backoff": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/backoff/-/backoff-2.5.0.tgz",
"integrity": "sha512-wC5ihrnUXmR2douXmXLCe5O3zg3GKIyvRi/hi58a/XyRxVI+3/yM0PYueQOZXPXQ9pxBislYkw+sF9b7C/RuMA==",
"dependencies": {
"precond": "0.2"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/balanced-match": { "node_modules/balanced-match": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -5331,6 +5367,11 @@
"node": ">= 10.0.0" "node": ">= 10.0.0"
} }
}, },
"node_modules/bcryptjs": {
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz",
"integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ=="
},
"node_modules/before-after-hook": { "node_modules/before-after-hook": {
"version": "2.2.3", "version": "2.2.3",
"resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz",
@@ -5815,6 +5856,11 @@
"node": ">=6.6.0" "node": ">=6.6.0"
} }
}, },
"node_modules/core-util-is": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
"integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ=="
},
"node_modules/create-hash": { "node_modules/create-hash": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
@@ -6885,6 +6931,14 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
}, },
"node_modules/extsprintf": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz",
"integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==",
"engines": [
"node >=0.6.0"
]
},
"node_modules/fast-content-type-parse": { "node_modules/fast-content-type-parse": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-1.1.0.tgz", "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-1.1.0.tgz",
@@ -8623,6 +8677,57 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/ldap-filter": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/ldap-filter/-/ldap-filter-0.3.3.tgz",
"integrity": "sha512-/tFkx5WIn4HuO+6w9lsfxq4FN3O+fDZeO9Mek8dCD8rTUpqzRa766BOBO7BcGkn3X86m5+cBm1/2S/Shzz7gMg==",
"dependencies": {
"assert-plus": "^1.0.0"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/ldapauth-fork": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/ldapauth-fork/-/ldapauth-fork-5.0.5.tgz",
"integrity": "sha512-LWUk76+V4AOZbny/3HIPQtGPWZyA3SW2tRhsWIBi9imP22WJktKLHV1ofd8Jo/wY7Ve6vAT7FCI5mEn3blZTjw==",
"dependencies": {
"@types/ldapjs": "^2.2.2",
"bcryptjs": "^2.4.0",
"ldapjs": "^2.2.1",
"lru-cache": "^7.10.1"
},
"engines": {
"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": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/ldapjs/-/ldapjs-2.3.3.tgz",
"integrity": "sha512-75QiiLJV/PQqtpH+HGls44dXweviFwQ6SiIK27EqzKQ5jU/7UFrl2E5nLdQ3IYRBzJ/AVFJI66u0MZ0uofKYwg==",
"dependencies": {
"abstract-logging": "^2.0.0",
"asn1": "^0.2.4",
"assert-plus": "^1.0.0",
"backoff": "^2.5.0",
"ldap-filter": "^0.3.3",
"once": "^1.4.0",
"vasync": "^2.2.0",
"verror": "^1.8.1"
},
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/leven": { "node_modules/leven": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz", "resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz",
@@ -9755,6 +9860,18 @@
"node": ">= 0.4.0" "node": ">= 0.4.0"
} }
}, },
"node_modules/passport-ldapauth": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/passport-ldapauth/-/passport-ldapauth-3.0.1.tgz",
"integrity": "sha512-TRRx3BHi8GC8MfCT9wmghjde/EGeKjll7zqHRRfGRxXbLcaDce2OftbQrFG7/AWaeFhR6zpZHtBQ/IkINdLVjQ==",
"dependencies": {
"ldapauth-fork": "^5.0.1",
"passport-strategy": "^1.0.0"
},
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/passport-oauth2": { "node_modules/passport-oauth2": {
"version": "1.7.0", "version": "1.7.0",
"resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.7.0.tgz", "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.7.0.tgz",
@@ -10309,6 +10426,14 @@
"node": ">=15.0.0" "node": ">=15.0.0"
} }
}, },
"node_modules/precond": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/precond/-/precond-0.2.3.tgz",
"integrity": "sha512-QCYG84SgGyGzqJ/vlMsxeXd/pgL/I94ixdNFyh1PusWmTCyVfPJjZ1K1jvHtsbfnXQs2TSkEP2fR7QiMZAnKFQ==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/prelude-ls": { "node_modules/prelude-ls": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@@ -12759,6 +12884,43 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/vasync": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/vasync/-/vasync-2.2.1.tgz",
"integrity": "sha512-Hq72JaTpcTFdWiNA4Y22Amej2GH3BFmBaKPPlDZ4/oC8HNn2ISHLkFrJU4Ds8R3jcUi7oo5Y9jcMHKjES+N9wQ==",
"engines": [
"node >=0.6.0"
],
"dependencies": {
"verror": "1.10.0"
}
},
"node_modules/vasync/node_modules/verror": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
"integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==",
"engines": [
"node >=0.6.0"
],
"dependencies": {
"assert-plus": "^1.0.0",
"core-util-is": "1.0.2",
"extsprintf": "^1.2.0"
}
},
"node_modules/verror": {
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz",
"integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==",
"dependencies": {
"assert-plus": "^1.0.0",
"core-util-is": "1.0.2",
"extsprintf": "^1.2.0"
},
"engines": {
"node": ">=0.6.0"
}
},
"node_modules/vite": { "node_modules/vite": {
"version": "5.0.12", "version": "5.0.12",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.0.12.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-5.0.12.tgz",
+1
View File
@@ -113,6 +113,7 @@
"passport-github": "^1.1.0", "passport-github": "^1.1.0",
"passport-gitlab2": "^5.0.0", "passport-gitlab2": "^5.0.0",
"passport-google-oauth20": "^2.0.0", "passport-google-oauth20": "^2.0.0",
"passport-ldapauth": "^3.0.1",
"pg": "^8.11.3", "pg": "^8.11.3",
"picomatch": "^3.0.1", "picomatch": "^3.0.1",
"pino": "^8.16.2", "pino": "^8.16.2",
+3
View File
@@ -3,6 +3,7 @@ import "fastify";
import { TUsers } from "@app/db/schemas"; import { TUsers } from "@app/db/schemas";
import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service"; import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service";
import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types"; import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types";
import { TLdapConfigServiceFactory } from "@app/ee/services/ldap-config/ldap-config-service";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { TSamlConfigServiceFactory } from "@app/ee/services/saml-config/saml-config-service"; import { TSamlConfigServiceFactory } from "@app/ee/services/saml-config/saml-config-service";
@@ -69,6 +70,7 @@ declare module "fastify" {
}; };
auditLogInfo: Pick<TCreateAuditLogDTO, "userAgent" | "userAgentType" | "ipAddress" | "actor">; auditLogInfo: Pick<TCreateAuditLogDTO, "userAgent" | "userAgentType" | "ipAddress" | "actor">;
ssoConfig: Awaited<ReturnType<TSamlConfigServiceFactory["getSaml"]>>; ssoConfig: Awaited<ReturnType<TSamlConfigServiceFactory["getSaml"]>>;
ldapConfig: Awaited<ReturnType<TLdapConfigServiceFactory["getLdapCfg"]>>;
} }
interface FastifyInstance { interface FastifyInstance {
@@ -107,6 +109,7 @@ declare module "fastify" {
snapshot: TSecretSnapshotServiceFactory; snapshot: TSecretSnapshotServiceFactory;
saml: TSamlConfigServiceFactory; saml: TSamlConfigServiceFactory;
scim: TScimServiceFactory; scim: TScimServiceFactory;
ldap: TLdapConfigServiceFactory;
auditLog: TAuditLogServiceFactory; auditLog: TAuditLogServiceFactory;
secretScanning: TSecretScanningServiceFactory; secretScanning: TSecretScanningServiceFactory;
license: TLicenseServiceFactory; license: TLicenseServiceFactory;
+8
View File
@@ -50,6 +50,9 @@ import {
TIntegrations, TIntegrations,
TIntegrationsInsert, TIntegrationsInsert,
TIntegrationsUpdate, TIntegrationsUpdate,
TLdapConfigs,
TLdapConfigsInsert,
TLdapConfigsUpdate,
TOrganizations, TOrganizations,
TOrganizationsInsert, TOrganizationsInsert,
TOrganizationsUpdate, TOrganizationsUpdate,
@@ -161,6 +164,9 @@ import {
TUserActions, TUserActions,
TUserActionsInsert, TUserActionsInsert,
TUserActionsUpdate, TUserActionsUpdate,
TUserAliases,
TUserAliasesInsert,
TUserAliasesUpdate,
TUserEncryptionKeys, TUserEncryptionKeys,
TUserEncryptionKeysInsert, TUserEncryptionKeysInsert,
TUserEncryptionKeysUpdate, TUserEncryptionKeysUpdate,
@@ -175,6 +181,7 @@ import {
declare module "knex/types/tables" { declare module "knex/types/tables" {
interface Tables { interface Tables {
[TableName.Users]: Knex.CompositeTableType<TUsers, TUsersInsert, TUsersUpdate>; [TableName.Users]: Knex.CompositeTableType<TUsers, TUsersInsert, TUsersUpdate>;
[TableName.UserAliases]: Knex.CompositeTableType<TUserAliases, TUserAliasesInsert, TUserAliasesUpdate>;
[TableName.UserEncryptionKey]: Knex.CompositeTableType< [TableName.UserEncryptionKey]: Knex.CompositeTableType<
TUserEncryptionKeys, TUserEncryptionKeys,
TUserEncryptionKeysInsert, TUserEncryptionKeysInsert,
@@ -318,6 +325,7 @@ declare module "knex/types/tables" {
TSecretSnapshotFoldersUpdate TSecretSnapshotFoldersUpdate
>; >;
[TableName.SamlConfig]: Knex.CompositeTableType<TSamlConfigs, TSamlConfigsInsert, TSamlConfigsUpdate>; [TableName.SamlConfig]: Knex.CompositeTableType<TSamlConfigs, TSamlConfigsInsert, TSamlConfigsUpdate>;
[TableName.LdapConfig]: Knex.CompositeTableType<TLdapConfigs, TLdapConfigsInsert, TLdapConfigsUpdate>;
[TableName.OrgBot]: Knex.CompositeTableType<TOrgBots, TOrgBotsInsert, TOrgBotsUpdate>; [TableName.OrgBot]: Knex.CompositeTableType<TOrgBots, TOrgBotsInsert, TOrgBotsUpdate>;
[TableName.AuditLog]: Knex.CompositeTableType<TAuditLogs, TAuditLogsInsert, TAuditLogsUpdate>; [TableName.AuditLog]: Knex.CompositeTableType<TAuditLogs, TAuditLogsInsert, TAuditLogsUpdate>;
[TableName.GitAppInstallSession]: Knex.CompositeTableType< [TableName.GitAppInstallSession]: Knex.CompositeTableType<
@@ -0,0 +1,68 @@
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.LdapConfig))) {
await knex.schema.createTable(TableName.LdapConfig, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.uuid("orgId").notNullable().unique();
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
t.boolean("isActive").notNullable();
t.string("url").notNullable();
t.string("encryptedBindDN").notNullable();
t.string("bindDNIV").notNullable();
t.string("bindDNTag").notNullable();
t.string("encryptedBindPass").notNullable();
t.string("bindPassIV").notNullable();
t.string("bindPassTag").notNullable();
t.string("searchBase").notNullable();
t.text("encryptedCACert").notNullable();
t.string("caCertIV").notNullable();
t.string("caCertTag").notNullable();
t.timestamps(true, true, true);
});
}
await createOnUpdateTrigger(knex, TableName.LdapConfig);
if (!(await knex.schema.hasTable(TableName.UserAliases))) {
await knex.schema.createTable(TableName.UserAliases, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.uuid("userId").notNullable();
t.foreign("userId").references("id").inTable(TableName.Users).onDelete("CASCADE");
t.string("username").notNullable();
t.string("aliasType").notNullable();
t.string("externalId").notNullable();
t.specificType("emails", "text[]");
t.uuid("orgId").nullable();
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
t.timestamps(true, true, true);
});
}
await createOnUpdateTrigger(knex, TableName.UserAliases);
await knex.schema.alterTable(TableName.Users, (t) => {
t.string("username").unique();
t.string("email").nullable().alter();
t.dropUnique(["email"]);
});
await knex(TableName.Users).update("username", knex.ref("email"));
await knex.schema.alterTable(TableName.Users, (t) => {
t.string("username").notNullable().alter();
});
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists(TableName.LdapConfig);
await knex.schema.dropTableIfExists(TableName.UserAliases);
await knex.schema.alterTable(TableName.Users, (t) => {
t.dropColumn("username");
// t.string("email").notNullable().alter();
});
await dropOnUpdateTrigger(knex, TableName.LdapConfig);
}
+2
View File
@@ -14,6 +14,7 @@ export * from "./identity-universal-auths";
export * from "./incident-contacts"; export * from "./incident-contacts";
export * from "./integration-auths"; export * from "./integration-auths";
export * from "./integrations"; export * from "./integrations";
export * from "./ldap-configs";
export * from "./models"; export * from "./models";
export * from "./org-bots"; export * from "./org-bots";
export * from "./org-memberships"; export * from "./org-memberships";
@@ -52,6 +53,7 @@ export * from "./service-tokens";
export * from "./super-admin"; export * from "./super-admin";
export * from "./trusted-ips"; export * from "./trusted-ips";
export * from "./user-actions"; export * from "./user-actions";
export * from "./user-aliases";
export * from "./user-encryption-keys"; export * from "./user-encryption-keys";
export * from "./users"; export * from "./users";
export * from "./webhooks"; export * from "./webhooks";
+31
View File
@@ -0,0 +1,31 @@
// 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 LdapConfigsSchema = z.object({
id: z.string().uuid(),
orgId: z.string().uuid(),
isActive: z.boolean(),
url: z.string(),
encryptedBindDN: z.string(),
bindDNIV: z.string(),
bindDNTag: z.string(),
encryptedBindPass: z.string(),
bindPassIV: z.string(),
bindPassTag: z.string(),
searchBase: z.string(),
encryptedCACert: z.string(),
caCertIV: z.string(),
caCertTag: z.string(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TLdapConfigs = z.infer<typeof LdapConfigsSchema>;
export type TLdapConfigsInsert = Omit<z.input<typeof LdapConfigsSchema>, TImmutableDBKeys>;
export type TLdapConfigsUpdate = Partial<Omit<z.input<typeof LdapConfigsSchema>, TImmutableDBKeys>>;
+2
View File
@@ -2,6 +2,7 @@ import { z } from "zod";
export enum TableName { export enum TableName {
Users = "users", Users = "users",
UserAliases = "user_aliases",
UserEncryptionKey = "user_encryption_keys", UserEncryptionKey = "user_encryption_keys",
AuthTokens = "auth_tokens", AuthTokens = "auth_tokens",
AuthTokenSession = "auth_token_sessions", AuthTokenSession = "auth_token_sessions",
@@ -50,6 +51,7 @@ export enum TableName {
SecretRotation = "secret_rotations", SecretRotation = "secret_rotations",
SecretRotationOutput = "secret_rotation_outputs", SecretRotationOutput = "secret_rotation_outputs",
SamlConfig = "saml_configs", SamlConfig = "saml_configs",
LdapConfig = "ldap_configs",
AuditLog = "audit_logs", AuditLog = "audit_logs",
GitAppInstallSession = "git_app_install_sessions", GitAppInstallSession = "git_app_install_sessions",
GitAppOrg = "git_app_org", GitAppOrg = "git_app_org",
+24
View File
@@ -0,0 +1,24 @@
// 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 UserAliasesSchema = z.object({
id: z.string().uuid(),
userId: z.string().uuid(),
username: z.string(),
aliasType: z.string(),
externalId: z.string(),
emails: z.string().array().nullable().optional(),
orgId: z.string().uuid().nullable().optional(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TUserAliases = z.infer<typeof UserAliasesSchema>;
export type TUserAliasesInsert = Omit<z.input<typeof UserAliasesSchema>, TImmutableDBKeys>;
export type TUserAliasesUpdate = Partial<Omit<z.input<typeof UserAliasesSchema>, TImmutableDBKeys>>;
+3 -2
View File
@@ -9,7 +9,7 @@ import { TImmutableDBKeys } from "./models";
export const UsersSchema = z.object({ export const UsersSchema = z.object({
id: z.string().uuid(), id: z.string().uuid(),
email: z.string(), email: z.string().nullable().optional(),
authMethods: z.string().array().nullable().optional(), authMethods: z.string().array().nullable().optional(),
superAdmin: z.boolean().default(false).nullable().optional(), superAdmin: z.boolean().default(false).nullable().optional(),
firstName: z.string().nullable().optional(), firstName: z.string().nullable().optional(),
@@ -20,7 +20,8 @@ export const UsersSchema = z.object({
devices: z.unknown().nullable().optional(), devices: z.unknown().nullable().optional(),
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date(), updatedAt: z.date(),
isGhost: z.boolean().default(false) isGhost: z.boolean().default(false),
username: z.string()
}); });
export type TUsers = z.infer<typeof UsersSchema>; export type TUsers = z.infer<typeof UsersSchema>;
+1
View File
@@ -21,6 +21,7 @@ export let userPublicKey: string | undefined;
export const seedData1 = { export const seedData1 = {
id: "3dafd81d-4388-432b-a4c5-f735616868c1", id: "3dafd81d-4388-432b-a4c5-f735616868c1",
username: process.env.TEST_USER_USERNAME || "[email protected]",
email: process.env.TEST_USER_EMAIL || "[email protected]", email: process.env.TEST_USER_EMAIL || "[email protected]",
password: process.env.TEST_USER_PASSWORD || "testInfisical@1", password: process.env.TEST_USER_PASSWORD || "testInfisical@1",
organization: { organization: {
+1
View File
@@ -22,6 +22,7 @@ export async function seed(knex: Knex): Promise<void> {
// eslint-disable-next-line // eslint-disable-next-line
// @ts-ignore // @ts-ignore
id: seedData1.id, id: seedData1.id,
username: seedData1.username,
email: seedData1.email, email: seedData1.email,
superAdmin: true, superAdmin: true,
firstName: "test", firstName: "test",
+2
View File
@@ -1,3 +1,4 @@
import { registerLdapRouter } from "./ldap-router";
import { registerLicenseRouter } from "./license-router"; import { registerLicenseRouter } from "./license-router";
import { registerOrgRoleRouter } from "./org-role-router"; import { registerOrgRoleRouter } from "./org-role-router";
import { registerProjectRoleRouter } from "./project-role-router"; import { registerProjectRoleRouter } from "./project-role-router";
@@ -35,6 +36,7 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
}); });
await server.register(registerSamlRouter, { prefix: "/sso" }); await server.register(registerSamlRouter, { prefix: "/sso" });
await server.register(registerScimRouter, { prefix: "/scim" }); await server.register(registerScimRouter, { prefix: "/scim" });
await server.register(registerLdapRouter, { prefix: "/ldap" });
await server.register(registerSecretScanningRouter, { prefix: "/secret-scanning" }); await server.register(registerSecretScanningRouter, { prefix: "/secret-scanning" });
await server.register(registerSecretRotationRouter, { prefix: "/secret-rotations" }); await server.register(registerSecretRotationRouter, { prefix: "/secret-rotations" });
await server.register(registerSecretVersionRouter, { prefix: "/secret" }); await server.register(registerSecretVersionRouter, { prefix: "/secret" });
+192
View File
@@ -0,0 +1,192 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-argument */
// All the any rules are disabled because passport typesense with fastify is really poor
import { IncomingMessage } from "node:http";
import { Authenticator } from "@fastify/passport";
import fastifySession from "@fastify/session";
import { FastifyRequest } from "fastify";
import LdapStrategy from "passport-ldapauth";
import { z } from "zod";
import { LdapConfigsSchema } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env";
import { logger } from "@app/lib/logger";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
export const registerLdapRouter = async (server: FastifyZodProvider) => {
const appCfg = getConfig();
const passport = new Authenticator({ key: "ldap", userProperty: "passportUser" });
await server.register(fastifySession, { secret: appCfg.COOKIE_SECRET_SIGN_KEY });
await server.register(passport.initialize());
await server.register(passport.secureSession());
const getLdapPassportOpts = (req: FastifyRequest, done: any) => {
const { organizationSlug } = req.body as {
organizationSlug: string;
};
process.nextTick(async () => {
try {
const { opts, ldapConfig } = await server.services.ldap.bootLdap(organizationSlug);
req.ldapConfig = ldapConfig;
done(null, opts);
} catch (err) {
done(err);
}
});
};
passport.use(
new LdapStrategy(
getLdapPassportOpts as any,
// eslint-disable-next-line
async (req: IncomingMessage, user, cb) => {
try {
const { isUserCompleted, providerAuthToken } = await server.services.ldap.ldapLogin({
externalId: user.uidNumber,
username: user.uid,
firstName: user.givenName,
lastName: user.sn,
emails: user.mail ? [user.mail] : [],
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);
}
}
)
);
server.route({
url: "/login",
method: "POST",
schema: {
body: z.object({
organizationSlug: z.string().trim()
})
},
preValidation: passport.authenticate("ldapauth", {
session: false
// failureFlash: true,
// failureRedirect: "/login/provider/error"
// this is due to zod type difference
}) as any,
handler: (req, res) => {
let nextUrl;
if (req.passportUser.isUserCompleted) {
nextUrl = `${appCfg.SITE_URL}/login/sso?token=${encodeURIComponent(req.passportUser.providerAuthToken)}`;
} else {
nextUrl = `${appCfg.SITE_URL}/signup/sso?token=${encodeURIComponent(req.passportUser.providerAuthToken)}`;
}
return res.status(200).send({
nextUrl
});
}
});
server.route({
url: "/config",
method: "GET",
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
querystring: z.object({
organizationId: z.string().trim()
}),
response: {
200: z.object({
id: z.string(),
organization: z.string(),
isActive: z.boolean(),
url: z.string(),
bindDN: z.string(),
bindPass: z.string(),
searchBase: z.string(),
caCert: z.string()
})
}
},
handler: async (req) => {
const ldap = await server.services.ldap.getLdapCfgWithPermissionCheck({
actor: req.permission.type,
actorId: req.permission.id,
orgId: req.query.organizationId,
actorOrgId: req.permission.orgId
});
return ldap;
}
});
server.route({
url: "/config",
method: "POST",
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
body: z.object({
organizationId: z.string().trim(),
isActive: z.boolean(),
url: z.string().trim(),
bindDN: z.string().trim(),
bindPass: z.string().trim(),
searchBase: z.string().trim(),
caCert: z.string().trim().default("")
}),
response: {
200: LdapConfigsSchema
}
},
handler: async (req) => {
const ldap = await server.services.ldap.createLdapCfg({
actor: req.permission.type,
actorId: req.permission.id,
orgId: req.body.organizationId,
actorOrgId: req.permission.orgId,
...req.body
});
return ldap;
}
});
server.route({
url: "/config",
method: "PATCH",
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
body: z.object({
organizationId: z.string().trim(),
isActive: z.boolean().optional(),
url: z.string().trim().optional(),
bindDN: z.string().trim().optional(),
bindPass: z.string().trim().optional(),
searchBase: z.string().trim().optional(),
caCert: z.string().trim().optional()
}),
response: {
200: LdapConfigsSchema
}
},
handler: async (req) => {
const ldap = await server.services.ldap.updateLdapCfg({
actor: req.permission.type,
actorId: req.permission.id,
orgId: req.body.organizationId,
actorOrgId: req.permission.orgId,
...req.body
});
return ldap;
}
});
};
+2 -2
View File
@@ -99,14 +99,14 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
async (req, profile, cb) => { async (req, profile, cb) => {
try { try {
if (!profile) throw new BadRequestError({ message: "Missing profile" }); if (!profile) throw new BadRequestError({ message: "Missing profile" });
const { firstName } = profile;
const email = profile?.email ?? (profile?.emailAddress as string); // emailRippling is added because in Rippling the field `email` reserved const email = profile?.email ?? (profile?.emailAddress as string); // emailRippling is added because in Rippling the field `email` reserved
if (!email || !firstName) { if (!profile.email || !profile.firstName) {
throw new BadRequestError({ message: "Invalid request. Missing email or first name" }); throw new BadRequestError({ message: "Invalid request. Missing email or first name" });
} }
const { isUserCompleted, providerAuthToken } = await server.services.saml.samlLogin({ const { isUserCompleted, providerAuthToken } = await server.services.saml.samlLogin({
username: profile.nameID ?? email,
email, email,
firstName: profile.firstName as string, firstName: profile.firstName as string,
lastName: profile.lastName as string, lastName: profile.lastName as string,
+15 -10
View File
@@ -122,7 +122,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
emails: z.array( emails: z.array(
z.object({ z.object({
primary: z.boolean(), primary: z.boolean(),
value: z.string().email(), value: z.string(),
type: z.string().trim() type: z.string().trim()
}) })
), ),
@@ -168,7 +168,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
emails: z.array( emails: z.array(
z.object({ z.object({
primary: z.boolean(), primary: z.boolean(),
value: z.string().email(), value: z.string(),
type: z.string().trim() type: z.string().trim()
}) })
), ),
@@ -198,13 +198,15 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
familyName: z.string().trim(), familyName: z.string().trim(),
givenName: z.string().trim() givenName: z.string().trim()
}), }),
// emails: z.array( // optional? emails: z
// z.object({ .array(
// primary: z.boolean(), z.object({
// value: z.string().email(), primary: z.boolean(),
// type: z.string().trim() value: z.string().email(),
// }) type: z.string().trim()
// ), })
)
.optional(),
// displayName: z.string().trim(), // displayName: z.string().trim(),
active: z.boolean() active: z.boolean()
}), }),
@@ -231,8 +233,11 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
}, },
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]), onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
handler: async (req) => { handler: async (req) => {
const primaryEmail = req.body.emails?.find((email) => email.primary)?.value;
const user = await req.server.services.scim.createScimUser({ const user = await req.server.services.scim.createScimUser({
email: req.body.userName, username: req.body.userName,
email: primaryEmail,
firstName: req.body.name.givenName, firstName: req.body.name.givenName,
lastName: req.body.name.familyName, lastName: req.body.name.familyName,
orgId: req.permission.orgId as string orgId: req.permission.orgId as string
@@ -92,7 +92,8 @@ export enum EventType {
interface UserActorMetadata { interface UserActorMetadata {
userId: string; userId: string;
email: string; email?: string | null;
username: string;
} }
interface ServiceActorMetadata { interface ServiceActorMetadata {
@@ -0,0 +1,11 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TLdapConfigDALFactory = ReturnType<typeof ldapConfigDALFactory>;
export const ldapConfigDALFactory = (db: TDbClient) => {
const ldapCfgOrm = ormify(db, TableName.LdapConfig);
return { ...ldapCfgOrm };
};
@@ -0,0 +1,429 @@
import { ForbiddenError } from "@casl/ability";
import jwt from "jsonwebtoken";
import { OrgMembershipRole, OrgMembershipStatus, SecretKeyEncoding, TLdapConfigsUpdate } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env";
import {
decryptSymmetric,
encryptSymmetric,
generateAsymmetricKeyPair,
generateSymmetricKey,
infisicalSymmetricDecrypt,
infisicalSymmetricEncypt
} from "@app/lib/crypto/encryption";
import { BadRequestError } from "@app/lib/errors";
import { TOrgPermission } from "@app/lib/types";
import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type";
import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal";
import { TOrgDALFactory } from "@app/services/org/org-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";
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, TLdapLoginDTO, TUpdateLdapCfgDTO } from "./ldap-config-types";
type TLdapConfigServiceFactoryDep = {
ldapConfigDAL: TLdapConfigDALFactory;
orgDAL: Pick<
TOrgDALFactory,
"createMembership" | "updateMembershipById" | "findMembership" | "findOrgById" | "findOne" | "updateById"
>;
orgBotDAL: Pick<TOrgBotDALFactory, "findOne" | "create" | "transaction">;
userDAL: Pick<TUserDALFactory, "create" | "findOne" | "transaction" | "updateById">;
userAliasDAL: Pick<TUserAliasDALFactory, "create" | "findOne">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
};
export type TLdapConfigServiceFactory = ReturnType<typeof ldapConfigServiceFactory>;
export const ldapConfigServiceFactory = ({
ldapConfigDAL,
orgDAL,
orgBotDAL,
userDAL,
userAliasDAL,
permissionService,
licenseService
}: TLdapConfigServiceFactoryDep) => {
const createLdapCfg = async ({
actor,
actorId,
orgId,
actorOrgId,
isActive,
url,
bindDN,
bindPass,
searchBase,
caCert
}: TCreateLdapCfgDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, 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 configuration due to plan restriction. Upgrade plan to create LDAP configuration."
});
const orgBot = await orgBotDAL.transaction(async (tx) => {
const doc = await orgBotDAL.findOne({ orgId }, tx);
if (doc) return doc;
const { privateKey, publicKey } = generateAsymmetricKeyPair();
const key = generateSymmetricKey();
const {
ciphertext: encryptedPrivateKey,
iv: privateKeyIV,
tag: privateKeyTag,
encoding: privateKeyKeyEncoding,
algorithm: privateKeyAlgorithm
} = infisicalSymmetricEncypt(privateKey);
const {
ciphertext: encryptedSymmetricKey,
iv: symmetricKeyIV,
tag: symmetricKeyTag,
encoding: symmetricKeyKeyEncoding,
algorithm: symmetricKeyAlgorithm
} = infisicalSymmetricEncypt(key);
return orgBotDAL.create(
{
name: "Infisical org bot",
publicKey,
privateKeyIV,
encryptedPrivateKey,
symmetricKeyIV,
symmetricKeyTag,
encryptedSymmetricKey,
symmetricKeyAlgorithm,
orgId,
privateKeyTag,
privateKeyAlgorithm,
privateKeyKeyEncoding,
symmetricKeyKeyEncoding
},
tx
);
});
const key = infisicalSymmetricDecrypt({
ciphertext: orgBot.encryptedSymmetricKey,
iv: orgBot.symmetricKeyIV,
tag: orgBot.symmetricKeyTag,
keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding
});
const { ciphertext: encryptedBindDN, iv: bindDNIV, tag: bindDNTag } = encryptSymmetric(bindDN, key);
const { ciphertext: encryptedBindPass, iv: bindPassIV, tag: bindPassTag } = encryptSymmetric(bindPass, key);
const { ciphertext: encryptedCACert, iv: caCertIV, tag: caCertTag } = encryptSymmetric(caCert, key);
const ldapConfig = await ldapConfigDAL.create({
orgId,
isActive,
url,
encryptedBindDN,
bindDNIV,
bindDNTag,
encryptedBindPass,
bindPassIV,
bindPassTag,
searchBase,
encryptedCACert,
caCertIV,
caCertTag
});
return ldapConfig;
};
const updateLdapCfg = async ({
actor,
actorId,
orgId,
actorOrgId,
isActive,
url,
bindDN,
bindPass,
searchBase,
caCert
}: TUpdateLdapCfgDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Ldap);
const plan = await licenseService.getPlan(orgId);
if (!plan.ldap)
throw new BadRequestError({
message:
"Failed to update LDAP configuration due to plan restriction. Upgrade plan to update LDAP configuration."
});
const updateQuery: TLdapConfigsUpdate = {
isActive,
url,
searchBase
};
const orgBot = await orgBotDAL.findOne({ orgId });
if (!orgBot) throw new BadRequestError({ message: "Org bot not found", name: "OrgBotNotFound" });
const key = infisicalSymmetricDecrypt({
ciphertext: orgBot.encryptedSymmetricKey,
iv: orgBot.symmetricKeyIV,
tag: orgBot.symmetricKeyTag,
keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding
});
if (bindDN) {
const { ciphertext: encryptedBindDN, iv: bindDNIV, tag: bindDNTag } = encryptSymmetric(bindDN, key);
updateQuery.encryptedBindDN = encryptedBindDN;
updateQuery.bindDNIV = bindDNIV;
updateQuery.bindDNTag = bindDNTag;
}
if (bindPass) {
const { ciphertext: encryptedBindPass, iv: bindPassIV, tag: bindPassTag } = encryptSymmetric(bindPass, key);
updateQuery.encryptedBindPass = encryptedBindPass;
updateQuery.bindPassIV = bindPassIV;
updateQuery.bindPassTag = bindPassTag;
}
if (caCert) {
const { ciphertext: encryptedCACert, iv: caCertIV, tag: caCertTag } = encryptSymmetric(caCert, key);
updateQuery.encryptedCACert = encryptedCACert;
updateQuery.caCertIV = caCertIV;
updateQuery.caCertTag = caCertTag;
}
const [ldapConfig] = await ldapConfigDAL.update({ orgId }, updateQuery);
return ldapConfig;
};
const getLdapCfg = async (filter: { orgId: string; isActive?: boolean }) => {
const ldapConfig = await ldapConfigDAL.findOne(filter);
if (!ldapConfig) throw new BadRequestError({ message: "Failed to find organization LDAP data" });
const orgBot = await orgBotDAL.findOne({ orgId: ldapConfig.orgId });
if (!orgBot) throw new BadRequestError({ message: "Org bot not found", name: "OrgBotNotFound" });
const key = infisicalSymmetricDecrypt({
ciphertext: orgBot.encryptedSymmetricKey,
iv: orgBot.symmetricKeyIV,
tag: orgBot.symmetricKeyTag,
keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding
});
const {
encryptedBindDN,
bindDNIV,
bindDNTag,
encryptedBindPass,
bindPassIV,
bindPassTag,
encryptedCACert,
caCertIV,
caCertTag
} = ldapConfig;
let bindDN = "";
if (encryptedBindDN && bindDNIV && bindDNTag) {
bindDN = decryptSymmetric({
ciphertext: encryptedBindDN,
key,
tag: bindDNTag,
iv: bindDNIV
});
}
let bindPass = "";
if (encryptedBindPass && bindPassIV && bindPassTag) {
bindPass = decryptSymmetric({
ciphertext: encryptedBindPass,
key,
tag: bindPassTag,
iv: bindPassIV
});
}
let caCert = "";
if (encryptedCACert && caCertIV && caCertTag) {
caCert = decryptSymmetric({
ciphertext: encryptedCACert,
key,
tag: caCertTag,
iv: caCertIV
});
}
return {
id: ldapConfig.id,
organization: ldapConfig.orgId,
isActive: ldapConfig.isActive,
url: ldapConfig.url,
bindDN,
bindPass,
searchBase: ldapConfig.searchBase,
caCert
};
};
const getLdapCfgWithPermissionCheck = async ({ actor, actorId, orgId, actorOrgId }: TOrgPermission) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Ldap);
return getLdapCfg({
orgId
});
};
const bootLdap = async (organizationSlug: string) => {
const organization = await orgDAL.findOne({ slug: organizationSlug });
if (!organization) throw new BadRequestError({ message: "Org not found" });
const ldapConfig = await getLdapCfg({
orgId: organization.id,
isActive: true
});
const opts = {
server: {
url: ldapConfig.url,
bindDN: ldapConfig.bindDN,
bindCredentials: ldapConfig.bindPass,
searchBase: ldapConfig.searchBase,
searchFilter: "(uid={{username}})",
searchAttributes: ["uid", "uidNumber", "givenName", "sn", "mail"],
...(ldapConfig.caCert !== ""
? {
tlsOptions: {
ca: [ldapConfig.caCert]
}
}
: {})
},
passReqToCallback: true
};
return { opts, ldapConfig };
};
const ldapLogin = async ({ externalId, username, firstName, lastName, emails, orgId, relayState }: TLdapLoginDTO) => {
const appCfg = getConfig();
let userAlias = await userAliasDAL.findOne({
externalId,
orgId,
aliasType: AuthMethod.LDAP
});
const organization = await orgDAL.findOrgById(orgId);
if (!organization) throw new BadRequestError({ message: "Org not found" });
if (userAlias) {
await userDAL.transaction(async (tx) => {
const [orgMembership] = await orgDAL.findMembership({ userId: userAlias.userId }, { tx });
if (!orgMembership) {
await orgDAL.createMembership(
{
userId: userAlias.userId,
orgId,
role: OrgMembershipRole.Member,
status: OrgMembershipStatus.Accepted
},
tx
);
} else if (orgMembership.status === OrgMembershipStatus.Invited) {
await orgDAL.updateMembershipById(
orgMembership.id,
{
status: OrgMembershipStatus.Accepted
},
tx
);
}
});
} else {
userAlias = await userDAL.transaction(async (tx) => {
const uniqueUsername = await normalizeUsername(username, userDAL);
const newUser = await userDAL.create(
{
username: uniqueUsername,
email: emails[0],
firstName,
lastName,
authMethods: [AuthMethod.LDAP],
isGhost: false
},
tx
);
const newUserAlias = await userAliasDAL.create(
{
userId: newUser.id,
username,
aliasType: AuthMethod.LDAP,
externalId,
emails,
orgId
},
tx
);
await orgDAL.createMembership(
{
userId: newUser.id,
orgId,
role: OrgMembershipRole.Member,
status: OrgMembershipStatus.Invited
},
tx
);
return newUserAlias;
});
}
const user = await userDAL.findOne({ id: userAlias.userId });
const isUserCompleted = Boolean(user.isAccepted);
const providerAuthToken = jwt.sign(
{
authTokenType: AuthTokenType.PROVIDER_TOKEN,
userId: user.id,
username: user.username,
firstName,
lastName,
organizationName: organization.name,
organizationId: organization.id,
authMethod: AuthMethod.LDAP,
isUserCompleted,
...(relayState
? {
callbackPort: (JSON.parse(relayState) as { callbackPort: string }).callbackPort
}
: {})
},
appCfg.AUTH_SECRET,
{
expiresIn: appCfg.JWT_PROVIDER_AUTH_LIFETIME
}
);
return { isUserCompleted, providerAuthToken };
};
return {
createLdapCfg,
updateLdapCfg,
getLdapCfgWithPermissionCheck,
getLdapCfg,
// getLdapPassportOpts,
ldapLogin,
bootLdap
};
};
@@ -0,0 +1,30 @@
import { TOrgPermission } from "@app/lib/types";
export type TCreateLdapCfgDTO = {
isActive: boolean;
url: string;
bindDN: string;
bindPass: string;
searchBase: string;
caCert: string;
} & TOrgPermission;
export type TUpdateLdapCfgDTO = Partial<{
isActive: boolean;
url: string;
bindDN: string;
bindPass: string;
searchBase: string;
caCert: string;
}> &
TOrgPermission;
export type TLdapLoginDTO = {
externalId: string;
username: string;
firstName: string;
lastName: string;
emails: string[];
orgId: string;
relayState?: string;
};
@@ -18,6 +18,8 @@ export const getDefaultOnPremFeatures = () => {
auditLogs: false, auditLogs: false,
auditLogsRetentionDays: 0, auditLogsRetentionDays: 0,
samlSSO: false, samlSSO: false,
scim: false,
ldap: false,
status: null, status: null,
trial_end: null, trial_end: null,
has_used_trial: true, has_used_trial: true,
@@ -25,6 +25,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
auditLogsRetentionDays: 0, auditLogsRetentionDays: 0,
samlSSO: false, samlSSO: false,
scim: false, scim: false,
ldap: false,
status: null, status: null,
trial_end: null, trial_end: null,
has_used_trial: true, has_used_trial: true,
@@ -147,14 +147,14 @@ export const licenseServiceFactory = ({
} }
}; };
const generateOrgCustomerId = async (orgName: string, email: string) => { const generateOrgCustomerId = async (orgName: string, email?: string | null) => {
if (instanceType === InstanceType.Cloud) { if (instanceType === InstanceType.Cloud) {
const { const {
data: { customerId } data: { customerId }
} = await licenseServerCloudApi.request.post<{ customerId: string }>( } = await licenseServerCloudApi.request.post<{ customerId: string }>(
"/api/license-server/v1/customers", "/api/license-server/v1/customers",
{ {
email, email: email ?? "",
name: orgName name: orgName
}, },
{ timeout: 5000, signal: AbortSignal.timeout(5000) } { timeout: 5000, signal: AbortSignal.timeout(5000) }
@@ -26,6 +26,7 @@ export type TFeatureSet = {
auditLogsRetentionDays: 0; auditLogsRetentionDays: 0;
samlSSO: false; samlSSO: false;
scim: false; scim: false;
ldap: false;
status: null; status: null;
trial_end: null; trial_end: null;
has_used_trial: true; has_used_trial: true;
@@ -17,6 +17,7 @@ export enum OrgPermissionSubjects {
IncidentAccount = "incident-contact", IncidentAccount = "incident-contact",
Sso = "sso", Sso = "sso",
Scim = "scim", Scim = "scim",
Ldap = "ldap",
Billing = "billing", Billing = "billing",
SecretScanning = "secret-scanning", SecretScanning = "secret-scanning",
Identity = "identity" Identity = "identity"
@@ -31,6 +32,7 @@ export type OrgPermissionSet =
| [OrgPermissionActions, OrgPermissionSubjects.IncidentAccount] | [OrgPermissionActions, OrgPermissionSubjects.IncidentAccount]
| [OrgPermissionActions, OrgPermissionSubjects.Sso] | [OrgPermissionActions, OrgPermissionSubjects.Sso]
| [OrgPermissionActions, OrgPermissionSubjects.Scim] | [OrgPermissionActions, OrgPermissionSubjects.Scim]
| [OrgPermissionActions, OrgPermissionSubjects.Ldap]
| [OrgPermissionActions, OrgPermissionSubjects.SecretScanning] | [OrgPermissionActions, OrgPermissionSubjects.SecretScanning]
| [OrgPermissionActions, OrgPermissionSubjects.Billing] | [OrgPermissionActions, OrgPermissionSubjects.Billing]
| [OrgPermissionActions, OrgPermissionSubjects.Identity]; | [OrgPermissionActions, OrgPermissionSubjects.Identity];
@@ -76,6 +78,11 @@ const buildAdminPermission = () => {
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Scim); can(OrgPermissionActions.Edit, OrgPermissionSubjects.Scim);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Scim); can(OrgPermissionActions.Delete, OrgPermissionSubjects.Scim);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Ldap);
can(OrgPermissionActions.Create, OrgPermissionSubjects.Ldap);
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Ldap);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Ldap);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Billing); can(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
can(OrgPermissionActions.Create, OrgPermissionSubjects.Billing); can(OrgPermissionActions.Create, OrgPermissionSubjects.Billing);
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Billing); can(OrgPermissionActions.Edit, OrgPermissionSubjects.Billing);
@@ -5,6 +5,7 @@ import {
OrgMembershipRole, OrgMembershipRole,
OrgMembershipStatus, OrgMembershipStatus,
SecretKeyEncoding, SecretKeyEncoding,
TableName,
TSamlConfigs, TSamlConfigs,
TSamlConfigsUpdate TSamlConfigsUpdate
} from "@app/db/schemas"; } from "@app/db/schemas";
@@ -31,7 +32,7 @@ import { TCreateSamlCfgDTO, TGetSamlCfgDTO, TSamlLoginDTO, TUpdateSamlCfgDTO } f
type TSamlConfigServiceFactoryDep = { type TSamlConfigServiceFactoryDep = {
samlConfigDAL: TSamlConfigDALFactory; samlConfigDAL: TSamlConfigDALFactory;
userDAL: Pick<TUserDALFactory, "create" | "findUserByEmail" | "transaction" | "updateById">; userDAL: Pick<TUserDALFactory, "create" | "findOne" | "transaction" | "updateById">;
orgDAL: Pick< orgDAL: Pick<
TOrgDALFactory, TOrgDALFactory,
"createMembership" | "updateMembershipById" | "findMembership" | "findOrgById" | "findOne" | "updateById" "createMembership" | "updateMembershipById" | "findMembership" | "findOrgById" | "findOne" | "updateById"
@@ -69,7 +70,7 @@ export const samlConfigServiceFactory = ({
if (!plan.samlSSO) if (!plan.samlSSO)
throw new BadRequestError({ throw new BadRequestError({
message: message:
"Failed to update SAML SSO configuration due to plan restriction. Upgrade plan to update SSO configuration." "Failed to create SAML SSO configuration due to plan restriction. Upgrade plan to create SSO configuration."
}); });
const orgBot = await orgBotDAL.transaction(async (tx) => { const orgBot = await orgBotDAL.transaction(async (tx) => {
@@ -122,7 +123,6 @@ export const samlConfigServiceFactory = ({
const { ciphertext: encryptedEntryPoint, iv: entryPointIV, tag: entryPointTag } = encryptSymmetric(entryPoint, key); const { ciphertext: encryptedEntryPoint, iv: entryPointIV, tag: entryPointTag } = encryptSymmetric(entryPoint, key);
const { ciphertext: encryptedIssuer, iv: issuerIV, tag: issuerTag } = encryptSymmetric(issuer, key); const { ciphertext: encryptedIssuer, iv: issuerIV, tag: issuerTag } = encryptSymmetric(issuer, key);
const { ciphertext: encryptedCert, iv: certIV, tag: certTag } = encryptSymmetric(cert, key); const { ciphertext: encryptedCert, iv: certIV, tag: certTag } = encryptSymmetric(cert, key);
const samlConfig = await samlConfigDAL.create({ const samlConfig = await samlConfigDAL.create({
orgId, orgId,
@@ -300,16 +300,30 @@ export const samlConfigServiceFactory = ({
}; };
}; };
const samlLogin = async ({ firstName, email, lastName, authProvider, orgId, relayState }: TSamlLoginDTO) => { const samlLogin = async ({
username,
email,
firstName,
lastName,
authProvider,
orgId,
relayState
}: TSamlLoginDTO) => {
const appCfg = getConfig(); const appCfg = getConfig();
let user = await userDAL.findUserByEmail(email); let user = await userDAL.findOne({ username });
const organization = await orgDAL.findOrgById(orgId); const organization = await orgDAL.findOrgById(orgId);
if (!organization) throw new BadRequestError({ message: "Org not found" }); if (!organization) throw new BadRequestError({ message: "Org not found" });
if (user) { if (user) {
await userDAL.transaction(async (tx) => { await userDAL.transaction(async (tx) => {
const [orgMembership] = await orgDAL.findMembership({ userId: user.id, orgId }, { tx }); const [orgMembership] = await orgDAL.findMembership(
{
userId: user.id,
[`${TableName.OrgMembership}.orgId` as "id"]: orgId
},
{ tx }
);
if (!orgMembership) { if (!orgMembership) {
await orgDAL.createMembership( await orgDAL.createMembership(
{ {
@@ -335,6 +349,7 @@ export const samlConfigServiceFactory = ({
user = await userDAL.transaction(async (tx) => { user = await userDAL.transaction(async (tx) => {
const newUser = await userDAL.create( const newUser = await userDAL.create(
{ {
username,
email, email,
firstName, firstName,
lastName, lastName,
@@ -357,7 +372,7 @@ export const samlConfigServiceFactory = ({
{ {
authTokenType: AuthTokenType.PROVIDER_TOKEN, authTokenType: AuthTokenType.PROVIDER_TOKEN,
userId: user.id, userId: user.id,
email: user.email, username: user.username,
firstName, firstName,
lastName, lastName,
organizationName: organization.name, organizationName: organization.name,
@@ -37,7 +37,8 @@ export type TGetSamlCfgDTO =
}; };
export type TSamlLoginDTO = { export type TSamlLoginDTO = {
email: string; username: string;
email?: string;
firstName: string; firstName: string;
lastName?: string; lastName?: string;
authProvider: string; authProvider: string;
+17 -11
View File
@@ -20,34 +20,38 @@ export const buildScimUserList = ({
export const buildScimUser = ({ export const buildScimUser = ({
userId, userId,
username,
email,
firstName, firstName,
lastName, lastName,
email,
active active
}: { }: {
userId: string; userId: string;
username: string;
email?: string | null;
firstName: string; firstName: string;
lastName: string; lastName: string;
email: string;
active: boolean; active: boolean;
}): TScimUser => { }): TScimUser => {
return { const scimUser = {
schemas: ["urn:ietf:params:scim:schemas:core:2.0:User"], schemas: ["urn:ietf:params:scim:schemas:core:2.0:User"],
id: userId, id: userId,
userName: email, userName: username,
displayName: `${firstName} ${lastName}`, displayName: `${firstName} ${lastName}`,
name: { name: {
givenName: firstName, givenName: firstName,
middleName: null, middleName: null,
familyName: lastName familyName: lastName
}, },
emails: [ emails: email
{ ? [
primary: true, {
value: email, primary: true,
type: "work" value: email,
} type: "work"
], }
]
: [],
active, active,
groups: [], groups: [],
meta: { meta: {
@@ -55,4 +59,6 @@ export const buildScimUser = ({
location: null location: null
} }
}; };
return scimUser;
}; };
+37 -22
View File
@@ -1,7 +1,7 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
import { OrgMembershipRole, OrgMembershipStatus } from "@app/db/schemas"; import { OrgMembershipRole, OrgMembershipStatus, TableName } from "@app/db/schemas";
import { TScimDALFactory } from "@app/ee/services/scim/scim-dal"; import { TScimDALFactory } from "@app/ee/services/scim/scim-dal";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { BadRequestError, ScimRequestError, UnauthorizedError } from "@app/lib/errors"; import { BadRequestError, ScimRequestError, UnauthorizedError } from "@app/lib/errors";
@@ -146,15 +146,16 @@ export const scimServiceFactory = ({
const users = await orgDAL.findMembership( const users = await orgDAL.findMembership(
{ {
orgId, [`${TableName.OrgMembership}.orgId` as "id"]: orgId,
...parseFilter(filter) ...parseFilter(filter)
}, },
findOpts findOpts
); );
const scimUsers = users.map(({ userId, firstName, lastName, email }) => const scimUsers = users.map(({ userId, username, firstName, lastName, email }) =>
buildScimUser({ buildScimUser({
userId: userId ?? "", userId: userId ?? "",
username,
firstName: firstName ?? "", firstName: firstName ?? "",
lastName: lastName ?? "", lastName: lastName ?? "",
email, email,
@@ -173,7 +174,7 @@ export const scimServiceFactory = ({
const [membership] = await orgDAL const [membership] = await orgDAL
.findMembership({ .findMembership({
userId, userId,
orgId [`${TableName.OrgMembership}.orgId` as "id"]: orgId
}) })
.catch(() => { .catch(() => {
throw new ScimRequestError({ throw new ScimRequestError({
@@ -196,14 +197,15 @@ export const scimServiceFactory = ({
return buildScimUser({ return buildScimUser({
userId: membership.userId as string, userId: membership.userId as string,
username: membership.username,
email: membership.email ?? "",
firstName: membership.firstName as string, firstName: membership.firstName as string,
lastName: membership.lastName as string, lastName: membership.lastName as string,
email: membership.email,
active: true active: true
}); });
}; };
const createScimUser = async ({ firstName, lastName, email, orgId }: TCreateScimUserDTO) => { const createScimUser = async ({ username, email, firstName, lastName, orgId }: TCreateScimUserDTO) => {
const org = await orgDAL.findById(orgId); const org = await orgDAL.findById(orgId);
if (!org) if (!org)
@@ -219,12 +221,18 @@ export const scimServiceFactory = ({
}); });
let user = await userDAL.findOne({ let user = await userDAL.findOne({
email username
}); });
if (user) { if (user) {
await userDAL.transaction(async (tx) => { await userDAL.transaction(async (tx) => {
const [orgMembership] = await orgDAL.findMembership({ userId: user.id, orgId }, { tx }); const [orgMembership] = await orgDAL.findMembership(
{
userId: user.id,
[`${TableName.OrgMembership}.orgId` as "id"]: orgId
},
{ tx }
);
if (orgMembership) if (orgMembership)
throw new ScimRequestError({ throw new ScimRequestError({
detail: "User already exists in the database", detail: "User already exists in the database",
@@ -248,6 +256,7 @@ export const scimServiceFactory = ({
user = await userDAL.transaction(async (tx) => { user = await userDAL.transaction(async (tx) => {
const newUser = await userDAL.create( const newUser = await userDAL.create(
{ {
username,
email, email,
firstName, firstName,
lastName, lastName,
@@ -272,21 +281,25 @@ export const scimServiceFactory = ({
} }
const appCfg = getConfig(); const appCfg = getConfig();
await smtpService.sendMail({
template: SmtpTemplates.ScimUserProvisioned, if (email) {
subjectLine: "Infisical organization invitation", await smtpService.sendMail({
recipients: [email], template: SmtpTemplates.ScimUserProvisioned,
substitutions: { subjectLine: "Infisical organization invitation",
organizationName: org.name, recipients: [email],
callback_url: `${appCfg.SITE_URL}/api/v1/sso/redirect/saml2/organizations/${org.slug}` substitutions: {
} organizationName: org.name,
}); callback_url: `${appCfg.SITE_URL}/api/v1/sso/redirect/saml2/organizations/${org.slug}`
}
});
}
return buildScimUser({ return buildScimUser({
userId: user.id, userId: user.id,
username: user.username,
firstName: user.firstName as string, firstName: user.firstName as string,
lastName: user.lastName as string, lastName: user.lastName as string,
email: user.email, email: user.email ?? "",
active: true active: true
}); });
}; };
@@ -295,7 +308,7 @@ export const scimServiceFactory = ({
const [membership] = await orgDAL const [membership] = await orgDAL
.findMembership({ .findMembership({
userId, userId,
orgId [`${TableName.OrgMembership}.orgId` as "id"]: orgId
}) })
.catch(() => { .catch(() => {
throw new ScimRequestError({ throw new ScimRequestError({
@@ -342,9 +355,10 @@ export const scimServiceFactory = ({
return buildScimUser({ return buildScimUser({
userId: membership.userId as string, userId: membership.userId as string,
username: membership.username,
email: membership.email,
firstName: membership.firstName as string, firstName: membership.firstName as string,
lastName: membership.lastName as string, lastName: membership.lastName as string,
email: membership.email,
active active
}); });
}; };
@@ -353,7 +367,7 @@ export const scimServiceFactory = ({
const [membership] = await orgDAL const [membership] = await orgDAL
.findMembership({ .findMembership({
userId, userId,
orgId [`${TableName.OrgMembership}.orgId` as "id"]: orgId
}) })
.catch(() => { .catch(() => {
throw new ScimRequestError({ throw new ScimRequestError({
@@ -387,9 +401,10 @@ export const scimServiceFactory = ({
return buildScimUser({ return buildScimUser({
userId: membership.userId as string, userId: membership.userId as string,
username: membership.username,
email: membership.email,
firstName: membership.firstName as string, firstName: membership.firstName as string,
lastName: membership.lastName as string, lastName: membership.lastName as string,
email: membership.email,
active active
}); });
}; };
+2 -1
View File
@@ -32,7 +32,8 @@ export type TGetScimUserDTO = {
}; };
export type TCreateScimUserDTO = { export type TCreateScimUserDTO = {
email: string; username: string;
email?: string;
firstName: string; firstName: string;
lastName: string; lastName: string;
orgId: string; orgId: string;
@@ -64,7 +64,7 @@ export const secretScanningQueueFactory = ({
orgId: organizationId, orgId: organizationId,
role: OrgMembershipRole.Admin role: OrgMembershipRole.Admin
}); });
return adminsOfWork.map((userObject) => userObject.email); return adminsOfWork.filter((userObject) => userObject.email).map((userObject) => userObject.email as string);
}; };
queueService.start(QueueName.SecretPushEventScan, async (job) => { queueService.start(QueueName.SecretPushEventScan, async (job) => {
@@ -149,7 +149,7 @@ export const secretScanningQueueFactory = ({
await smtpService.sendMail({ await smtpService.sendMail({
template: SmtpTemplates.SecretLeakIncident, template: SmtpTemplates.SecretLeakIncident,
subjectLine: `Incident alert: leaked secrets found in Github repository ${repository.fullName}`, subjectLine: `Incident alert: leaked secrets found in Github repository ${repository.fullName}`,
recipients: adminEmails, recipients: adminEmails.filter((email) => email).map((email) => email),
substitutions: { substitutions: {
numberOfSecrets: Object.keys(allFindingsByFingerprint).length, numberOfSecrets: Object.keys(allFindingsByFingerprint).length,
pusher_email: pusher.email, pusher_email: pusher.email,
@@ -221,7 +221,7 @@ export const secretScanningQueueFactory = ({
await smtpService.sendMail({ await smtpService.sendMail({
template: SmtpTemplates.SecretLeakIncident, template: SmtpTemplates.SecretLeakIncident,
subjectLine: `Incident alert: leaked secrets found in Github repository ${repository.fullName}`, subjectLine: `Incident alert: leaked secrets found in Github repository ${repository.fullName}`,
recipients: adminEmails, recipients: adminEmails.filter((email) => email).map((email) => email),
substitutions: { substitutions: {
numberOfSecrets: findings.length numberOfSecrets: findings.length
} }
+1 -1
View File
@@ -5,7 +5,7 @@ import { ActorType } from "@app/services/auth/auth-type";
// this is a unique id for sending posthog event // this is a unique id for sending posthog event
export const getTelemetryDistinctId = (req: FastifyRequest) => { export const getTelemetryDistinctId = (req: FastifyRequest) => {
if (req.auth.actor === ActorType.USER) { if (req.auth.actor === ActorType.USER) {
return req.auth.user.email; return req.auth.user.username;
} }
if (req.auth.actor === ActorType.IDENTITY) { if (req.auth.actor === ActorType.IDENTITY) {
return `identity-${req.auth.identityId}`; return `identity-${req.auth.identityId}`;
+1
View File
@@ -44,6 +44,7 @@ export const injectAuditLogInfo = fp(async (server: FastifyZodProvider) => {
type: ActorType.USER, type: ActorType.USER,
metadata: { metadata: {
email: req.auth.user.email, email: req.auth.user.email,
username: req.auth.user.username,
userId: req.permission.id userId: req.permission.id
} }
}; };
+17 -1
View File
@@ -5,6 +5,8 @@ import { registerV1EERoutes } from "@app/ee/routes/v1";
import { auditLogDALFactory } from "@app/ee/services/audit-log/audit-log-dal"; import { auditLogDALFactory } from "@app/ee/services/audit-log/audit-log-dal";
import { auditLogQueueServiceFactory } from "@app/ee/services/audit-log/audit-log-queue"; import { auditLogQueueServiceFactory } from "@app/ee/services/audit-log/audit-log-queue";
import { auditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service"; import { auditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service";
import { ldapConfigDALFactory } from "@app/ee/services/ldap-config/ldap-config-dal";
import { ldapConfigServiceFactory } from "@app/ee/services/ldap-config/ldap-config-service";
import { licenseDALFactory } from "@app/ee/services/license/license-dal"; import { licenseDALFactory } from "@app/ee/services/license/license-dal";
import { licenseServiceFactory } from "@app/ee/services/license/license-service"; import { licenseServiceFactory } from "@app/ee/services/license/license-service";
import { permissionDALFactory } from "@app/ee/services/permission/permission-dal"; import { permissionDALFactory } from "@app/ee/services/permission/permission-dal";
@@ -102,6 +104,7 @@ import { telemetryQueueServiceFactory } from "@app/services/telemetry/telemetry-
import { telemetryServiceFactory } from "@app/services/telemetry/telemetry-service"; import { telemetryServiceFactory } from "@app/services/telemetry/telemetry-service";
import { userDALFactory } from "@app/services/user/user-dal"; import { userDALFactory } from "@app/services/user/user-dal";
import { userServiceFactory } from "@app/services/user/user-service"; import { userServiceFactory } from "@app/services/user/user-service";
import { userAliasDALFactory } from "@app/services/user-alias/user-alias-dal";
import { webhookDALFactory } from "@app/services/webhook/webhook-dal"; import { webhookDALFactory } from "@app/services/webhook/webhook-dal";
import { webhookServiceFactory } from "@app/services/webhook/webhook-service"; import { webhookServiceFactory } from "@app/services/webhook/webhook-service";
@@ -126,6 +129,7 @@ export const registerRoutes = async (
// db layers // db layers
const userDAL = userDALFactory(db); const userDAL = userDALFactory(db);
const userAliasDAL = userAliasDALFactory(db);
const authDAL = authDALFactory(db); const authDAL = authDALFactory(db);
const authTokenDAL = tokenDALFactory(db); const authTokenDAL = tokenDALFactory(db);
const orgDAL = orgDALFactory(db); const orgDAL = orgDALFactory(db);
@@ -166,12 +170,13 @@ export const registerRoutes = async (
const auditLogDAL = auditLogDALFactory(db); const auditLogDAL = auditLogDALFactory(db);
const trustedIpDAL = trustedIpDALFactory(db); const trustedIpDAL = trustedIpDALFactory(db);
const scimDAL = scimDALFactory(db);
const telemetryDAL = telemetryDALFactory(db); const telemetryDAL = telemetryDALFactory(db);
// ee db layer ops // ee db layer ops
const permissionDAL = permissionDALFactory(db); const permissionDAL = permissionDALFactory(db);
const samlConfigDAL = samlConfigDALFactory(db); const samlConfigDAL = samlConfigDALFactory(db);
const scimDAL = scimDALFactory(db);
const ldapConfigDAL = ldapConfigDALFactory(db);
const sapApproverDAL = secretApprovalPolicyApproverDALFactory(db); const sapApproverDAL = secretApprovalPolicyApproverDALFactory(db);
const secretApprovalPolicyDAL = secretApprovalPolicyDALFactory(db); const secretApprovalPolicyDAL = secretApprovalPolicyDALFactory(db);
const secretApprovalRequestDAL = secretApprovalRequestDALFactory(db); const secretApprovalRequestDAL = secretApprovalRequestDALFactory(db);
@@ -235,6 +240,16 @@ export const registerRoutes = async (
smtpService smtpService
}); });
const ldapService = ldapConfigServiceFactory({
ldapConfigDAL,
orgDAL,
orgBotDAL,
userDAL,
userAliasDAL,
permissionService,
licenseService
});
const telemetryService = telemetryServiceFactory({ const telemetryService = telemetryServiceFactory({
keyStore, keyStore,
licenseService licenseService
@@ -561,6 +576,7 @@ export const registerRoutes = async (
secretRotation: secretRotationService, secretRotation: secretRotationService,
snapshot: snapshotService, snapshot: snapshotService,
saml: samlService, saml: samlService,
ldap: ldapService,
auditLog: auditLogService, auditLog: auditLogService,
secretScanning: secretScanningService, secretScanning: secretScanningService,
license: licenseService, license: licenseService,
+3 -2
View File
@@ -92,9 +92,10 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
await server.services.telemetry.sendPostHogEvents({ await server.services.telemetry.sendPostHogEvents({
event: PostHogEventTypes.AdminInit, event: PostHogEventTypes.AdminInit,
distinctId: user.user.email, distinctId: user.user.username ?? "",
properties: { properties: {
email: user.user.email, username: user.user.username,
email: user.user.email ?? "",
lastName: user.user.lastName || "", lastName: user.user.lastName || "",
firstName: user.user.firstName || "" firstName: user.user.firstName || ""
} }
@@ -58,6 +58,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
users: OrgMembershipsSchema.merge( users: OrgMembershipsSchema.merge(
z.object({ z.object({
user: UsersSchema.pick({ user: UsersSchema.pick({
username: true,
email: true, email: true,
firstName: true, firstName: true,
lastName: true, lastName: true,
@@ -63,6 +63,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
users: ProjectMembershipsSchema.merge( users: ProjectMembershipsSchema.merge(
z.object({ z.object({
user: UsersSchema.pick({ user: UsersSchema.pick({
username: true,
email: true, email: true,
firstName: true, firstName: true,
lastName: true, lastName: true,
@@ -24,6 +24,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
users: OrgMembershipsSchema.merge( users: OrgMembershipsSchema.merge(
z.object({ z.object({
user: UsersSchema.pick({ user: UsersSchema.pick({
username: true,
email: true, email: true,
firstName: true, firstName: true,
lastName: true, lastName: true,
@@ -179,11 +180,12 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
handler: async (req) => { handler: async (req) => {
if (req.auth.actor !== ActorType.USER) return; if (req.auth.actor !== ActorType.USER) return;
const organization = await server.services.org.createOrganization( const organization = await server.services.org.createOrganization({
req.permission.id, userId: req.permission.id,
req.auth.user.email, userEmail: req.auth.user.email,
req.body.name orgName: req.body.name
); });
return { organization }; return { organization };
} }
}); });
@@ -14,7 +14,8 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
projectId: z.string().describe("The ID of the project.") projectId: z.string().describe("The ID of the project.")
}), }),
body: z.object({ body: z.object({
emails: z.string().email().array().describe("Emails of the users to add to the project.") emails: z.string().email().array().default([]).describe("Emails of the users to add to the project."),
usernames: z.string().array().default([]).describe("Usernames of the users to add to the project.")
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -28,7 +29,8 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
projectId: req.params.projectId, projectId: req.params.projectId,
actorId: req.permission.id, actorId: req.permission.id,
actor: req.permission.type, actor: req.permission.type,
emails: req.body.emails emails: req.body.emails,
usernames: req.body.usernames
}); });
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
@@ -57,7 +59,8 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
}), }),
body: z.object({ body: z.object({
emails: z.string().email().array().describe("Emails of the users to remove from the project.") emails: z.string().email().array().default([]).describe("Emails of the users to remove from the project."),
usernames: z.string().array().default([]).describe("Usernames of the users to remove from the project.")
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -72,7 +75,8 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
actor: req.permission.type, actor: req.permission.type,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
projectId: req.params.projectId, projectId: req.params.projectId,
emails: req.body.emails emails: req.body.emails,
usernames: req.body.usernames
}); });
for (const membership of memberships) { for (const membership of memberships) {
+2 -2
View File
@@ -12,7 +12,7 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
}, },
schema: { schema: {
body: z.object({ body: z.object({
email: z.string().email().trim(), email: z.string().trim(),
providerAuthToken: z.string().trim().optional(), providerAuthToken: z.string().trim().optional(),
clientPublicKey: z.string().trim() clientPublicKey: z.string().trim()
}), }),
@@ -42,7 +42,7 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
}, },
schema: { schema: {
body: z.object({ body: z.object({
email: z.string().email().trim(), email: z.string().trim(),
providerAuthToken: z.string().trim().optional(), providerAuthToken: z.string().trim().optional(),
clientProof: z.string().trim() clientProof: z.string().trim()
}), }),
+13 -7
View File
@@ -88,7 +88,7 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => {
}, },
schema: { schema: {
body: z.object({ body: z.object({
email: z.string().email().trim(), email: z.string().trim(),
firstName: z.string().trim(), firstName: z.string().trim(),
lastName: z.string().trim().optional(), lastName: z.string().trim().optional(),
protectedKey: z.string().trim(), protectedKey: z.string().trim(),
@@ -131,13 +131,16 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => {
authorization: req.headers.authorization as string authorization: req.headers.authorization as string
}); });
void server.services.telemetry.sendLoopsEvent(user.email, user.firstName || "", user.lastName || ""); if (user.email) {
void server.services.telemetry.sendLoopsEvent(user.email, user.firstName || "", user.lastName || "");
}
void server.services.telemetry.sendPostHogEvents({ void server.services.telemetry.sendPostHogEvents({
event: PostHogEventTypes.UserSignedUp, event: PostHogEventTypes.UserSignedUp,
distinctId: user.email, distinctId: user.username ?? "",
properties: { properties: {
email: user.email, username: user.username,
email: user.email ?? "",
attributionSource: req.body.attributionSource attributionSource: req.body.attributionSource
} }
}); });
@@ -194,13 +197,16 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => {
authorization: req.headers.authorization as string authorization: req.headers.authorization as string
}); });
void server.services.telemetry.sendLoopsEvent(user.email, user.firstName || "", user.lastName || ""); if (user.email) {
void server.services.telemetry.sendLoopsEvent(user.email, user.firstName || "", user.lastName || "");
}
void server.services.telemetry.sendPostHogEvents({ void server.services.telemetry.sendPostHogEvents({
event: PostHogEventTypes.UserSignedUp, event: PostHogEventTypes.UserSignedUp,
distinctId: user.email, distinctId: user.username ?? "",
properties: { properties: {
email: user.email, username: user.username,
email: user.email ?? "",
attributionSource: "Team Invite" attributionSource: "Team Invite"
} }
}); });
+3 -2
View File
@@ -5,13 +5,14 @@ import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
import { AuthModeProviderJwtTokenPayload, AuthModeProviderSignUpTokenPayload, AuthTokenType } from "./auth-type"; import { AuthModeProviderJwtTokenPayload, AuthModeProviderSignUpTokenPayload, AuthTokenType } from "./auth-type";
export const validateProviderAuthToken = (providerToken: string, email: string) => { export const validateProviderAuthToken = (providerToken: string, username?: string) => {
if (!providerToken) throw new UnauthorizedError(); if (!providerToken) throw new UnauthorizedError();
const appCfg = getConfig(); const appCfg = getConfig();
const decodedToken = jwt.verify(providerToken, appCfg.AUTH_SECRET) as AuthModeProviderJwtTokenPayload; const decodedToken = jwt.verify(providerToken, appCfg.AUTH_SECRET) as AuthModeProviderJwtTokenPayload;
if (decodedToken.authTokenType !== AuthTokenType.PROVIDER_TOKEN) throw new UnauthorizedError(); if (decodedToken.authTokenType !== AuthTokenType.PROVIDER_TOKEN) throw new UnauthorizedError();
if (decodedToken.email !== email) throw new Error("Invalid auth credentials");
if (decodedToken.username !== username) throw new Error("Invalid auth credentials");
if (decodedToken.organizationId) { if (decodedToken.organizationId) {
return { orgId: decodedToken.organizationId }; return { orgId: decodedToken.organizationId };
+31 -18
View File
@@ -39,17 +39,19 @@ export const authLoginServiceFactory = ({ userDAL, tokenService, smtpService }:
if (!isDeviceSeen) { if (!isDeviceSeen) {
const newDeviceList = devices.concat([{ ip, userAgent }]); const newDeviceList = devices.concat([{ ip, userAgent }]);
await userDAL.updateById(user.id, { devices: JSON.stringify(newDeviceList) }); await userDAL.updateById(user.id, { devices: JSON.stringify(newDeviceList) });
await smtpService.sendMail({ if (user.email) {
template: SmtpTemplates.NewDeviceJoin, await smtpService.sendMail({
subjectLine: "Successful login from new device", template: SmtpTemplates.NewDeviceJoin,
recipients: [user.email], subjectLine: "Successful login from new device",
substitutions: { recipients: [user.email],
email: user.email, substitutions: {
timestamp: new Date().toString(), email: user.email,
ip, timestamp: new Date().toString(),
userAgent ip,
} userAgent
}); }
});
}
} }
}; };
@@ -131,7 +133,9 @@ export const authLoginServiceFactory = ({ userDAL, tokenService, smtpService }:
providerAuthToken, providerAuthToken,
clientPublicKey clientPublicKey
}: TLoginGenServerPublicKeyDTO) => { }: TLoginGenServerPublicKeyDTO) => {
const userEnc = await userDAL.findUserEncKeyByEmail(email); const userEnc = await userDAL.findUserEncKeyByUsername({
username: email
});
if (!userEnc || (userEnc && !userEnc.isAccepted)) { if (!userEnc || (userEnc && !userEnc.isAccepted)) {
throw new Error("Failed to find user"); throw new Error("Failed to find user");
} }
@@ -158,7 +162,9 @@ export const authLoginServiceFactory = ({ userDAL, tokenService, smtpService }:
ip, ip,
userAgent userAgent
}: TLoginClientProofDTO) => { }: TLoginClientProofDTO) => {
const userEnc = await userDAL.findUserEncKeyByEmail(email); const userEnc = await userDAL.findUserEncKeyByUsername({
username: email
});
if (!userEnc) throw new Error("Failed to find user"); if (!userEnc) throw new Error("Failed to find user");
const cfg = getConfig(); const cfg = getConfig();
@@ -187,7 +193,7 @@ export const authLoginServiceFactory = ({ userDAL, tokenService, smtpService }:
clientPublicKey: null clientPublicKey: null
}); });
// send multi factor auth token if they it enabled // send multi factor auth token if they it enabled
if (userEnc.isMfaEnabled) { if (userEnc.isMfaEnabled && userEnc.email) {
const mfaToken = jwt.sign( const mfaToken = jwt.sign(
{ {
authTokenType: AuthTokenType.MFA_TOKEN, authTokenType: AuthTokenType.MFA_TOKEN,
@@ -227,7 +233,7 @@ export const authLoginServiceFactory = ({ userDAL, tokenService, smtpService }:
*/ */
const resendMfaToken = async (userId: string) => { const resendMfaToken = async (userId: string) => {
const user = await userDAL.findById(userId); const user = await userDAL.findById(userId);
if (!user) return; if (!user || !user.email) return;
await sendUserMfaCode({ await sendUserMfaCode({
userId: user.id, userId: user.id,
email: user.email email: user.email
@@ -263,7 +269,7 @@ export const authLoginServiceFactory = ({ userDAL, tokenService, smtpService }:
* OAuth2 login for google,github, and other oauth2 provider * OAuth2 login for google,github, and other oauth2 provider
* */ * */
const oauth2Login = async ({ email, firstName, lastName, authMethod, callbackPort }: TOauthLoginDTO) => { const oauth2Login = async ({ email, firstName, lastName, authMethod, callbackPort }: TOauthLoginDTO) => {
let user = await userDAL.findUserByEmail(email); let user = await userDAL.findUserByUsername(email);
const serverCfg = await getServerCfg(); const serverCfg = await getServerCfg();
const appCfg = getConfig(); const appCfg = getConfig();
@@ -282,7 +288,14 @@ export const authLoginServiceFactory = ({ userDAL, tokenService, smtpService }:
}); });
} }
user = await userDAL.create({ email, firstName, lastName, authMethods: [authMethod], isGhost: false }); user = await userDAL.create({
username: email,
email,
firstName,
lastName,
authMethods: [authMethod],
isGhost: false
});
} }
const isLinkingRequired = !user?.authMethods?.includes(authMethod); const isLinkingRequired = !user?.authMethods?.includes(authMethod);
const isUserCompleted = user.isAccepted; const isUserCompleted = user.isAccepted;
@@ -290,7 +303,7 @@ export const authLoginServiceFactory = ({ userDAL, tokenService, smtpService }:
{ {
authTokenType: AuthTokenType.PROVIDER_TOKEN, authTokenType: AuthTokenType.PROVIDER_TOKEN,
userId: user.id, userId: user.id,
email: user.email, username: user.username,
firstName: user.firstName, firstName: user.firstName,
lastName: user.lastName, lastName: user.lastName,
authMethod, authMethod,
@@ -99,7 +99,7 @@ export const authPaswordServiceFactory = ({
* Email password reset flow via email. Step 1 send email * Email password reset flow via email. Step 1 send email
*/ */
const sendPasswordResetEmail = async (email: string) => { const sendPasswordResetEmail = async (email: string) => {
const user = await userDAL.findUserByEmail(email); const user = await userDAL.findUserByUsername(email);
// ignore as user is not found to avoid an outside entity to identify infisical registered accounts // ignore as user is not found to avoid an outside entity to identify infisical registered accounts
if (!user || (user && !user.isAccepted)) return; if (!user || (user && !user.isAccepted)) return;
@@ -126,7 +126,7 @@ export const authPaswordServiceFactory = ({
* */ * */
const verifyPasswordResetEmail = async (email: string, code: string) => { const verifyPasswordResetEmail = async (email: string, code: string) => {
const cfg = getConfig(); const cfg = getConfig();
const user = await userDAL.findUserByEmail(email); const user = await userDAL.findUserByUsername(email);
// ignore as user is not found to avoid an outside entity to identify infisical registered accounts // ignore as user is not found to avoid an outside entity to identify infisical registered accounts
if (!user || (user && !user.isAccepted)) { if (!user || (user && !user.isAccepted)) {
throw new Error("Failed email verification for pass reset"); throw new Error("Failed email verification for pass reset");
@@ -44,13 +44,13 @@ export const authSignupServiceFactory = ({
throw new Error("Provided a disposable email"); throw new Error("Provided a disposable email");
} }
let user = await userDAL.findUserByEmail(email); let user = await userDAL.findUserByUsername(email);
if (user && user.isAccepted) { if (user && user.isAccepted) {
// TODO(akhilmhdh-pg): copy as old one. this needs to be changed due to security issues // TODO(akhilmhdh-pg): copy as old one. this needs to be changed due to security issues
throw new Error("Failed to send verification code for complete account"); throw new Error("Failed to send verification code for complete account");
} }
if (!user) { if (!user) {
user = await userDAL.create({ authMethods: [AuthMethod.EMAIL], email, isGhost: false }); user = await userDAL.create({ authMethods: [AuthMethod.EMAIL], username: email, email, isGhost: false });
} }
if (!user) throw new Error("Failed to create user"); if (!user) throw new Error("Failed to create user");
@@ -70,7 +70,7 @@ export const authSignupServiceFactory = ({
}; };
const verifyEmailSignup = async (email: string, code: string) => { const verifyEmailSignup = async (email: string, code: string) => {
const user = await userDAL.findUserByEmail(email); const user = await userDAL.findUserByUsername(email);
if (!user || (user && user.isAccepted)) { if (!user || (user && user.isAccepted)) {
// TODO(akhilmhdh): copy as old one. this needs to be changed due to security issues // TODO(akhilmhdh): copy as old one. this needs to be changed due to security issues
throw new Error("Failed to send verification code for complete account"); throw new Error("Failed to send verification code for complete account");
@@ -115,14 +115,14 @@ export const authSignupServiceFactory = ({
userAgent, userAgent,
authorization authorization
}: TCompleteAccountSignupDTO) => { }: TCompleteAccountSignupDTO) => {
const user = await userDAL.findUserByEmail(email); const user = await userDAL.findOne({ username: email });
if (!user || (user && user.isAccepted)) { if (!user || (user && user.isAccepted)) {
throw new Error("Failed to complete account for complete user"); throw new Error("Failed to complete account for complete user");
} }
let organizationId; let organizationId;
if (providerAuthToken) { if (providerAuthToken) {
const { orgId } = validateProviderAuthToken(providerAuthToken, user.email); const { orgId } = validateProviderAuthToken(providerAuthToken, user.username);
organizationId = orgId; organizationId = orgId;
} else { } else {
validateSignUpAuthorization(authorization, user.id); validateSignUpAuthorization(authorization, user.id);
@@ -150,7 +150,11 @@ export const authSignupServiceFactory = ({
}); });
if (!organizationId) { if (!organizationId) {
await orgService.createOrganization(user.id, user.email, organizationName); await orgService.createOrganization({
userId: user.id,
userEmail: user.email ?? user.username,
orgName: organizationName
});
} }
const updatedMembersips = await orgDAL.updateMembership( const updatedMembersips = await orgDAL.updateMembership(
@@ -215,7 +219,7 @@ export const authSignupServiceFactory = ({
encryptedPrivateKeyTag, encryptedPrivateKeyTag,
authorization authorization
}: TCompleteAccountInviteDTO) => { }: TCompleteAccountInviteDTO) => {
const user = await userDAL.findUserByEmail(email); const user = await userDAL.findUserByUsername(email);
if (!user || (user && user.isAccepted)) { if (!user || (user && user.isAccepted)) {
throw new Error("Failed to complete account for complete user"); throw new Error("Failed to complete account for complete user");
} }
+3 -2
View File
@@ -5,7 +5,8 @@ export enum AuthMethod {
GITLAB = "gitlab", GITLAB = "gitlab",
OKTA_SAML = "okta-saml", OKTA_SAML = "okta-saml",
AZURE_SAML = "azure-saml", AZURE_SAML = "azure-saml",
JUMPCLOUD_SAML = "jumpcloud-saml" JUMPCLOUD_SAML = "jumpcloud-saml",
LDAP = "ldap"
} }
export enum AuthTokenType { export enum AuthTokenType {
@@ -61,7 +62,7 @@ export type AuthModeRefreshJwtTokenPayload = {
export type AuthModeProviderJwtTokenPayload = { export type AuthModeProviderJwtTokenPayload = {
authTokenType: AuthTokenType.PROVIDER_TOKEN; authTokenType: AuthTokenType.PROVIDER_TOKEN;
email: string; username: string;
organizationId?: string; organizationId?: string;
}; };
+14 -8
View File
@@ -57,7 +57,7 @@ export const orgDALFactory = (db: TDbClient) => {
const findAllOrgMembers = async (orgId: string) => { const findAllOrgMembers = async (orgId: string) => {
try { try {
const members = await db(TableName.OrgMembership) const members = await db(TableName.OrgMembership)
.where({ orgId }) .where(`${TableName.OrgMembership}.orgId`, orgId)
.join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`) .join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`)
.leftJoin<TUserEncryptionKeys>( .leftJoin<TUserEncryptionKeys>(
TableName.UserEncryptionKey, TableName.UserEncryptionKey,
@@ -72,25 +72,27 @@ export const orgDALFactory = (db: TDbClient) => {
db.ref("roleId").withSchema(TableName.OrgMembership), db.ref("roleId").withSchema(TableName.OrgMembership),
db.ref("status").withSchema(TableName.OrgMembership), db.ref("status").withSchema(TableName.OrgMembership),
db.ref("email").withSchema(TableName.Users), db.ref("email").withSchema(TableName.Users),
db.ref("username").withSchema(TableName.Users),
db.ref("firstName").withSchema(TableName.Users), db.ref("firstName").withSchema(TableName.Users),
db.ref("lastName").withSchema(TableName.Users), db.ref("lastName").withSchema(TableName.Users),
db.ref("id").withSchema(TableName.Users).as("userId"), db.ref("id").withSchema(TableName.Users).as("userId"),
db.ref("publicKey").withSchema(TableName.UserEncryptionKey) db.ref("publicKey").withSchema(TableName.UserEncryptionKey)
) )
.where({ isGhost: false }); // MAKE SURE USER IS NOT A GHOST USER .where({ isGhost: false }); // MAKE SURE USER IS NOT A GHOST USER
return members.map(({ email, firstName, lastName, userId, publicKey, ...data }) => ({
return members.map(({ email, username, firstName, lastName, userId, publicKey, ...data }) => ({
...data, ...data,
user: { email, firstName, lastName, id: userId, publicKey } user: { email, username, firstName, lastName, id: userId, publicKey }
})); }));
} catch (error) { } catch (error) {
throw new DatabaseError({ error, name: "Find all org members" }); throw new DatabaseError({ error, name: "Find all org members" });
} }
}; };
const findOrgMembersByEmail = async (orgId: string, emails: string[]) => { const findOrgMembersByUsername = async (orgId: string, usernames: string[]) => {
try { try {
const members = await db(TableName.OrgMembership) const members = await db(TableName.OrgMembership)
.where({ orgId }) .where(`${TableName.OrgMembership}.orgId`, orgId)
.join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`) .join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`)
.leftJoin<TUserEncryptionKeys>( .leftJoin<TUserEncryptionKeys>(
TableName.UserEncryptionKey, TableName.UserEncryptionKey,
@@ -104,6 +106,7 @@ export const orgDALFactory = (db: TDbClient) => {
db.ref("role").withSchema(TableName.OrgMembership), db.ref("role").withSchema(TableName.OrgMembership),
db.ref("roleId").withSchema(TableName.OrgMembership), db.ref("roleId").withSchema(TableName.OrgMembership),
db.ref("status").withSchema(TableName.OrgMembership), db.ref("status").withSchema(TableName.OrgMembership),
db.ref("username").withSchema(TableName.Users),
db.ref("email").withSchema(TableName.Users), db.ref("email").withSchema(TableName.Users),
db.ref("firstName").withSchema(TableName.Users), db.ref("firstName").withSchema(TableName.Users),
db.ref("lastName").withSchema(TableName.Users), db.ref("lastName").withSchema(TableName.Users),
@@ -111,7 +114,7 @@ export const orgDALFactory = (db: TDbClient) => {
db.ref("publicKey").withSchema(TableName.UserEncryptionKey) db.ref("publicKey").withSchema(TableName.UserEncryptionKey)
) )
.where({ isGhost: false }) .where({ isGhost: false })
.whereIn("email", emails); .whereIn("username", usernames);
return members.map(({ email, firstName, lastName, userId, publicKey, ...data }) => ({ return members.map(({ email, firstName, lastName, userId, publicKey, ...data }) => ({
...data, ...data,
user: { email, firstName, lastName, id: userId, publicKey } user: { email, firstName, lastName, id: userId, publicKey }
@@ -243,10 +246,13 @@ export const orgDALFactory = (db: TDbClient) => {
.select( .select(
selectAllTableCols(TableName.OrgMembership), selectAllTableCols(TableName.OrgMembership),
db.ref("email").withSchema(TableName.Users), db.ref("email").withSchema(TableName.Users),
db.ref("username").withSchema(TableName.Users),
db.ref("firstName").withSchema(TableName.Users), db.ref("firstName").withSchema(TableName.Users),
db.ref("lastName").withSchema(TableName.Users), db.ref("lastName").withSchema(TableName.Users),
db.ref("scimEnabled").withSchema(TableName.Organization) db.ref("scimEnabled").withSchema(TableName.Organization)
); )
.where({ isGhost: false });
if (limit) void query.limit(limit); if (limit) void query.limit(limit);
if (offset) void query.offset(offset); if (offset) void query.offset(offset);
if (sort) { if (sort) {
@@ -266,7 +272,7 @@ export const orgDALFactory = (db: TDbClient) => {
findOrgById, findOrgById,
findAllOrgsByUserId, findAllOrgsByUserId,
ghostUserExists, ghostUserExists,
findOrgMembersByEmail, findOrgMembersByUsername,
findOrgGhostUser, findOrgGhostUser,
create, create,
updateById, updateById,
+17 -7
View File
@@ -103,11 +103,11 @@ export const orgServiceFactory = ({
return members; return members;
}; };
const findOrgMembersByEmail = async ({ actor, actorId, orgId, emails }: TFindOrgMembersByEmailDTO) => { const findOrgMembersByUsername = async ({ actor, actorId, orgId, emails }: TFindOrgMembersByEmailDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId); const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Member); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Member);
const members = await orgDAL.findOrgMembersByEmail(orgId, emails); const members = await orgDAL.findOrgMembersByUsername(orgId, emails);
return members; return members;
}; };
@@ -145,6 +145,7 @@ export const orgServiceFactory = ({
{ {
isGhost: true, isGhost: true,
authMethods: [AuthMethod.EMAIL], authMethods: [AuthMethod.EMAIL],
username: email,
email, email,
isAccepted: true isAccepted: true
}, },
@@ -239,7 +240,15 @@ export const orgServiceFactory = ({
/* /*
* Create organization * Create organization
* */ * */
const createOrganization = async (userId: string, userEmail: string, orgName: string) => { const createOrganization = async ({
userId,
userEmail,
orgName
}: {
userId: string;
orgName: string;
userEmail?: string | null;
}) => {
const { privateKey, publicKey } = generateAsymmetricKeyPair(); const { privateKey, publicKey } = generateAsymmetricKeyPair();
const key = generateSymmetricKey(); const key = generateSymmetricKey();
const { const {
@@ -367,7 +376,7 @@ export const orgServiceFactory = ({
}); });
} }
const invitee = await orgDAL.transaction(async (tx) => { const invitee = await orgDAL.transaction(async (tx) => {
const inviteeUser = await userDAL.findUserByEmail(inviteeEmail, tx); const inviteeUser = await userDAL.findUserByUsername(inviteeEmail, tx);
if (inviteeUser) { if (inviteeUser) {
// if user already exist means its already part of infisical // if user already exist means its already part of infisical
// Thus the signup flow is not needed anymore // Thus the signup flow is not needed anymore
@@ -403,6 +412,7 @@ export const orgServiceFactory = ({
// not invited before // not invited before
const user = await userDAL.create( const user = await userDAL.create(
{ {
username: inviteeEmail,
email: inviteeEmail, email: inviteeEmail,
isAccepted: false, isAccepted: false,
authMethods: [AuthMethod.EMAIL], authMethods: [AuthMethod.EMAIL],
@@ -437,7 +447,7 @@ export const orgServiceFactory = ({
recipients: [inviteeEmail], recipients: [inviteeEmail],
substitutions: { substitutions: {
inviterFirstName: user.firstName, inviterFirstName: user.firstName,
inviterEmail: user.email, inviterUsername: user.username,
organizationName: org?.name, organizationName: org?.name,
email: inviteeEmail, email: inviteeEmail,
organizationId: org?.id.toString(), organizationId: org?.id.toString(),
@@ -457,7 +467,7 @@ export const orgServiceFactory = ({
* magic link and issue a temporary signup token for user to complete setting up their account * magic link and issue a temporary signup token for user to complete setting up their account
*/ */
const verifyUserToOrg = async ({ orgId, email, code }: TVerifyUserToOrgDTO) => { const verifyUserToOrg = async ({ orgId, email, code }: TVerifyUserToOrgDTO) => {
const user = await userDAL.findUserByEmail(email); const user = await userDAL.findUserByUsername(email);
if (!user) { if (!user) {
throw new BadRequestError({ message: "Invalid request", name: "Verify user to org" }); throw new BadRequestError({ message: "Invalid request", name: "Verify user to org" });
} }
@@ -595,7 +605,7 @@ export const orgServiceFactory = ({
inviteUserToOrganization, inviteUserToOrganization,
verifyUserToOrg, verifyUserToOrg,
updateOrg, updateOrg,
findOrgMembersByEmail, findOrgMembersByUsername,
createOrganization, createOrganization,
deleteOrganizationById, deleteOrganizationById,
deleteOrgMembership, deleteOrgMembership,
@@ -25,6 +25,7 @@ export const projectMembershipDALFactory = (db: TDbClient) => {
db.ref("role").withSchema(TableName.ProjectMembership), db.ref("role").withSchema(TableName.ProjectMembership),
db.ref("roleId").withSchema(TableName.ProjectMembership), db.ref("roleId").withSchema(TableName.ProjectMembership),
db.ref("isGhost").withSchema(TableName.Users), db.ref("isGhost").withSchema(TableName.Users),
db.ref("username").withSchema(TableName.Users),
db.ref("email").withSchema(TableName.Users), db.ref("email").withSchema(TableName.Users),
db.ref("publicKey").withSchema(TableName.UserEncryptionKey), db.ref("publicKey").withSchema(TableName.UserEncryptionKey),
db.ref("firstName").withSchema(TableName.Users), db.ref("firstName").withSchema(TableName.Users),
@@ -32,9 +33,9 @@ export const projectMembershipDALFactory = (db: TDbClient) => {
db.ref("id").withSchema(TableName.Users).as("userId") db.ref("id").withSchema(TableName.Users).as("userId")
) )
.where({ isGhost: false }); .where({ isGhost: false });
return members.map(({ email, firstName, lastName, publicKey, isGhost, ...data }) => ({ return members.map(({ username, email, firstName, lastName, publicKey, isGhost, ...data }) => ({
...data, ...data,
user: { email, firstName, lastName, id: data.userId, publicKey, isGhost } user: { username, email, firstName, lastName, id: data.userId, publicKey, isGhost }
})); }));
} catch (error) { } catch (error) {
throw new DatabaseError({ error, name: "Find all project members" }); throw new DatabaseError({ error, name: "Find all project members" });
@@ -56,7 +57,7 @@ export const projectMembershipDALFactory = (db: TDbClient) => {
} }
}; };
const findMembershipsByEmail = async (projectId: string, emails: string[]) => { const findMembershipsByUsername = async (projectId: string, usernames: string[]) => {
try { try {
const members = await db(TableName.ProjectMembership) const members = await db(TableName.ProjectMembership)
.where({ projectId }) .where({ projectId })
@@ -69,13 +70,13 @@ export const projectMembershipDALFactory = (db: TDbClient) => {
.select( .select(
selectAllTableCols(TableName.ProjectMembership), selectAllTableCols(TableName.ProjectMembership),
db.ref("id").withSchema(TableName.Users).as("userId"), db.ref("id").withSchema(TableName.Users).as("userId"),
db.ref("email").withSchema(TableName.Users) db.ref("username").withSchema(TableName.Users)
) )
.whereIn("email", emails) .whereIn("username", usernames)
.where({ isGhost: false }); .where({ isGhost: false });
return members.map(({ userId, email, ...data }) => ({ return members.map(({ userId, username, ...data }) => ({
...data, ...data,
user: { id: userId, email } user: { id: userId, username }
})); }));
} catch (error) { } catch (error) {
throw new DatabaseError({ error, name: "Find members by email" }); throw new DatabaseError({ error, name: "Find members by email" });
@@ -100,7 +101,7 @@ export const projectMembershipDALFactory = (db: TDbClient) => {
...projectMemberOrm, ...projectMemberOrm,
findAllProjectMembers, findAllProjectMembers,
findProjectGhostUser, findProjectGhostUser,
findMembershipsByEmail, findMembershipsByUsername,
findProjectMembershipsByUserId findProjectMembershipsByUserId
}; };
}; };
@@ -45,7 +45,7 @@ type TProjectMembershipServiceFactoryDep = {
projectMembershipDAL: TProjectMembershipDALFactory; projectMembershipDAL: TProjectMembershipDALFactory;
userDAL: Pick<TUserDALFactory, "findById" | "findOne" | "findUserByProjectMembershipId" | "find">; userDAL: Pick<TUserDALFactory, "findById" | "findOne" | "findUserByProjectMembershipId" | "find">;
projectRoleDAL: Pick<TProjectRoleDALFactory, "findOne">; projectRoleDAL: Pick<TProjectRoleDALFactory, "findOne">;
orgDAL: Pick<TOrgDALFactory, "findMembership" | "findOrgMembersByEmail">; orgDAL: Pick<TOrgDALFactory, "findMembership" | "findOrgMembersByUsername">;
projectDAL: Pick<TProjectDALFactory, "findById" | "findProjectGhostUser" | "transaction">; projectDAL: Pick<TProjectDALFactory, "findById" | "findProjectGhostUser" | "transaction">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "findLatestProjectKey" | "delete" | "insertMany">; projectKeyDAL: Pick<TProjectKeyDALFactory, "findLatestProjectKey" | "delete" | "insertMany">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
@@ -134,8 +134,8 @@ export const projectMembershipServiceFactory = ({
const appCfg = getConfig(); const appCfg = getConfig();
await smtpService.sendMail({ await smtpService.sendMail({
template: SmtpTemplates.WorkspaceInvite, template: SmtpTemplates.WorkspaceInvite,
subjectLine: "Infisical workspace invitation", subjectLine: "Infisical project invitation",
recipients: invitees.map((i) => i.email), recipients: invitees.filter((i) => i.email).map((i) => i.email as string),
substitutions: { substitutions: {
workspaceName: project.name, workspaceName: project.name,
callback_url: `${appCfg.SITE_URL}/login` callback_url: `${appCfg.SITE_URL}/login`
@@ -206,8 +206,8 @@ export const projectMembershipServiceFactory = ({
const appCfg = getConfig(); const appCfg = getConfig();
await smtpService.sendMail({ await smtpService.sendMail({
template: SmtpTemplates.WorkspaceInvite, template: SmtpTemplates.WorkspaceInvite,
subjectLine: "Infisical workspace invitation", subjectLine: "Infisical project invitation",
recipients: orgMembers.map(({ email }) => email).filter(Boolean), recipients: orgMembers.filter((i) => i.email).map((i) => i.email as string),
substitutions: { substitutions: {
workspaceName: project.name, workspaceName: project.name,
callback_url: `${appCfg.SITE_URL}/login` callback_url: `${appCfg.SITE_URL}/login`
@@ -222,6 +222,7 @@ export const projectMembershipServiceFactory = ({
actorId, actorId,
actor, actor,
emails, emails,
usernames,
sendEmails = true sendEmails = true
}: TAddUsersToWorkspaceNonE2EEDTO) => { }: TAddUsersToWorkspaceNonE2EEDTO) => {
const project = await projectDAL.findById(projectId); const project = await projectDAL.findById(projectId);
@@ -234,9 +235,14 @@ export const projectMembershipServiceFactory = ({
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId); const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Member); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Member);
const orgMembers = await orgDAL.findOrgMembersByEmail(project.orgId, emails); const usernamesAndEmails = [...emails, ...usernames];
if (orgMembers.length !== emails.length) throw new BadRequestError({ message: "Some users are not part of org" }); const orgMembers = await orgDAL.findOrgMembersByUsername(project.orgId, [
...new Set(usernamesAndEmails.map((element) => element.toLowerCase()))
]);
if (orgMembers.length !== usernamesAndEmails.length)
throw new BadRequestError({ message: "Some users are not part of org" });
if (!orgMembers.length) return []; if (!orgMembers.length) return [];
@@ -315,16 +321,21 @@ export const projectMembershipServiceFactory = ({
}); });
if (sendEmails) { if (sendEmails) {
const recipients = orgMembers.filter((i) => i.user.email).map((i) => i.user.email as string);
const appCfg = getConfig(); const appCfg = getConfig();
await smtpService.sendMail({
template: SmtpTemplates.WorkspaceInvite, if (recipients.length) {
subjectLine: "Infisical workspace invitation", await smtpService.sendMail({
recipients: orgMembers.map(({ user }) => user.email).filter(Boolean), template: SmtpTemplates.WorkspaceInvite,
substitutions: { subjectLine: "Infisical project invitation",
workspaceName: project.name, recipients: orgMembers.filter((i) => i.user.email).map((i) => i.user.email as string),
callback_url: `${appCfg.SITE_URL}/login` substitutions: {
} workspaceName: project.name,
}); callback_url: `${appCfg.SITE_URL}/login`
}
});
}
} }
return members; return members;
}; };
@@ -407,7 +418,8 @@ export const projectMembershipServiceFactory = ({
actor, actor,
actorOrgId, actorOrgId,
projectId, projectId,
emails emails,
usernames
}: TDeleteProjectMembershipsDTO) => { }: TDeleteProjectMembershipsDTO) => {
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Member); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Member);
@@ -421,9 +433,13 @@ export const projectMembershipServiceFactory = ({
}); });
} }
const projectMembers = await projectMembershipDAL.findMembershipsByEmail(projectId, emails); const usernamesAndEmails = [...emails, ...usernames];
if (projectMembers.length !== emails.length) { const projectMembers = await projectMembershipDAL.findMembershipsByUsername(projectId, [
...new Set(usernamesAndEmails.map((element) => element.toLowerCase()))
]);
if (projectMembers.length !== usernamesAndEmails.length) {
throw new BadRequestError({ throw new BadRequestError({
message: "Some users are not part of project", message: "Some users are not part of project",
name: "Delete project membership" name: "Delete project membership"
@@ -18,6 +18,7 @@ export type TDeleteProjectMembershipOldDTO = {
export type TDeleteProjectMembershipsDTO = { export type TDeleteProjectMembershipsDTO = {
emails: string[]; emails: string[];
usernames: string[];
} & TProjectPermission; } & TProjectPermission;
export type TAddUsersToWorkspaceDTO = { export type TAddUsersToWorkspaceDTO = {
@@ -33,4 +34,5 @@ export type TAddUsersToWorkspaceDTO = {
export type TAddUsersToWorkspaceNonE2EEDTO = { export type TAddUsersToWorkspaceNonE2EEDTO = {
sendEmails?: boolean; sendEmails?: boolean;
emails: string[]; emails: string[];
usernames: string[];
} & TProjectPermission; } & TProjectPermission;
+1 -1
View File
@@ -391,7 +391,7 @@ export const secretQueueFactory = ({
await smtpService.sendMail({ await smtpService.sendMail({
template: SmtpTemplates.SecretReminder, template: SmtpTemplates.SecretReminder,
subjectLine: "Infisical secret reminder", subjectLine: "Infisical secret reminder",
recipients: [...projectMembers.map((m) => m.user.email)], recipients: [...projectMembers.map((m) => m.user.email)].filter((email) => email).map((email) => email as string),
substitutions: { substitutions: {
reminderNote: data.note, // May not be present. reminderNote: data.note, // May not be present.
projectName: project.name, projectName: project.name,
@@ -8,7 +8,7 @@
</head> </head>
<body> <body>
<h2>Join your organization on Infisical</h2> <h2>Join your organization on Infisical</h2>
<p>{{inviterFirstName}} ({{inviterEmail}}) has invited you to their Infisical organization — {{organizationName}}</p> <p>{{inviterFirstName}} ({{inviterUsername}}) has invited you to their Infisical organization — {{organizationName}}</p>
<a href="{{callback_url}}?token={{token}}&to={{email}}&organization_id={{organizationId}}">Join now</a> <a href="{{callback_url}}?token={{token}}&to={{email}}&organization_id={{organizationId}}">Join now</a>
<h3>What is Infisical?</h3> <h3>What is Infisical?</h3>
<p>Infisical is an easy-to-use end-to-end encrypted tool that enables developers to sync and manage their secrets and configs.</p> <p>Infisical is an easy-to-use end-to-end encrypted tool that enables developers to sync and manage their secrets and configs.</p>
@@ -97,6 +97,7 @@ export const superAdminServiceFactory = ({
{ {
firstName, firstName,
lastName, lastName,
username: email,
email, email,
superAdmin: true, superAdmin: true,
isGhost: false, isGhost: false,
@@ -126,11 +127,11 @@ export const superAdminServiceFactory = ({
const initialOrganizationName = appCfg.INITIAL_ORGANIZATION_NAME ?? "Admin Org"; const initialOrganizationName = appCfg.INITIAL_ORGANIZATION_NAME ?? "Admin Org";
const organization = await orgService.createOrganization( const organization = await orgService.createOrganization({
userInfo.user.id, userId: userInfo.user.id,
userInfo.user.email, userEmail: userInfo.user.email,
initialOrganizationName orgName: initialOrganizationName
); });
await updateServerCfg({ initialized: true }); await updateServerCfg({ initialized: true });
const token = await authService.generateUserTokens({ const token = await authService.generateUserTokens({
@@ -37,6 +37,7 @@ export type TSecretModifiedEvent = {
export type TAdminInitEvent = { export type TAdminInitEvent = {
event: PostHogEventTypes.AdminInit; event: PostHogEventTypes.AdminInit;
properties: { properties: {
username: string;
email: string; email: string;
firstName: string; firstName: string;
lastName: string; lastName: string;
@@ -46,6 +47,7 @@ export type TAdminInitEvent = {
export type TUserSignedUpEvent = { export type TUserSignedUpEvent = {
event: PostHogEventTypes.UserSignedUp; event: PostHogEventTypes.UserSignedUp;
properties: { properties: {
username: string;
email: string; email: string;
attributionSource?: string; attributionSource?: string;
}; };
@@ -0,0 +1,13 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TUserAliasDALFactory = ReturnType<typeof userAliasDALFactory>;
export const userAliasDALFactory = (db: TDbClient) => {
const userAliasOrm = ormify(db, TableName.UserAliases);
return {
...userAliasOrm
};
};
+8 -5
View File
@@ -16,14 +16,17 @@ export type TUserDALFactory = ReturnType<typeof userDALFactory>;
export const userDALFactory = (db: TDbClient) => { export const userDALFactory = (db: TDbClient) => {
const userOrm = ormify(db, TableName.Users); const userOrm = ormify(db, TableName.Users);
const findUserByEmail = async (email: string, tx?: Knex) => userOrm.findOne({ email }, tx); const findUserByUsername = async (username: string, tx?: Knex) => userOrm.findOne({ username }, tx);
// USER ENCRYPTION FUNCTIONS // USER ENCRYPTION FUNCTIONS
// ------------------------- // -------------------------
const findUserEncKeyByEmail = async (email: string) => { const findUserEncKeyByUsername = async ({ username }: { username: string }) => {
try { try {
return await db(TableName.Users) return await db(TableName.Users)
.where({ email, isGhost: false }) .where({
username,
isGhost: false
})
.join(TableName.UserEncryptionKey, `${TableName.Users}.id`, `${TableName.UserEncryptionKey}.userId`) .join(TableName.UserEncryptionKey, `${TableName.Users}.id`, `${TableName.UserEncryptionKey}.userId`)
.first(); .first();
} catch (error) { } catch (error) {
@@ -118,8 +121,8 @@ export const userDALFactory = (db: TDbClient) => {
return { return {
...userOrm, ...userOrm,
findUserByEmail, findUserByUsername,
findUserEncKeyByEmail, findUserEncKeyByUsername,
findUserEncKeyByUserId, findUserEncKeyByUserId,
updateUserEncryptionByUserId, updateUserEncryptionByUserId,
findUserByProjectMembershipId, findUserByProjectMembershipId,
+21
View File
@@ -0,0 +1,21 @@
import slugify from "@sindresorhus/slugify";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { TUserDALFactory } from "@app/services/user/user-dal";
export const normalizeUsername = async (username: string, userDAL: Pick<TUserDALFactory, "findOne">) => {
let attempt = slugify(username);
let user = await userDAL.findOne({ username: attempt });
if (!user) return attempt;
while (true) {
attempt = slugify(`${username}-${alphaNumericNanoId(4)}`);
// eslint-disable-next-line no-await-in-loop
user = await userDAL.findOne({ username: attempt });
if (!user) {
return attempt;
}
}
};
+10
View File
@@ -11,6 +11,10 @@ export type TUserServiceFactory = ReturnType<typeof userServiceFactory>;
export const userServiceFactory = ({ userDAL }: TUserServiceFactoryDep) => { export const userServiceFactory = ({ userDAL }: TUserServiceFactoryDep) => {
const toggleUserMfa = async (userId: string, isMfaEnabled: boolean) => { const toggleUserMfa = async (userId: string, isMfaEnabled: boolean) => {
const user = await userDAL.findById(userId);
if (!user || !user.email) throw new BadRequestError({ name: "Failed to toggle MFA" });
const updatedUser = await userDAL.updateById(userId, { const updatedUser = await userDAL.updateById(userId, {
isMfaEnabled, isMfaEnabled,
mfaMethods: isMfaEnabled ? ["email"] : [] mfaMethods: isMfaEnabled ? ["email"] : []
@@ -30,6 +34,12 @@ export const userServiceFactory = ({ userDAL }: TUserServiceFactoryDep) => {
const user = await userDAL.findById(userId); const user = await userDAL.findById(userId);
if (!user) throw new BadRequestError({ name: "Update auth methods" }); if (!user) throw new BadRequestError({ name: "Update auth methods" });
if (user.authMethods?.includes(AuthMethod.LDAP))
throw new BadRequestError({ message: "LDAP auth method cannot be updated", name: "Update auth methods" });
if (authMethods.includes(AuthMethod.LDAP))
throw new BadRequestError({ message: "LDAP auth method cannot be updated", name: "Update auth methods" });
const updatedUser = await userDAL.updateById(userId, { authMethods }); const updatedUser = await userDAL.updateById(userId, { authMethods });
return updatedUser; return updatedUser;
}; };
+2 -1
View File
@@ -59,7 +59,8 @@ var exportCmd = &cobra.Command{
util.HandleError(err, "Unable to parse flag") util.HandleError(err, "Unable to parse flag")
} }
infisicalToken, err := cmd.Flags().GetString("token") infisicalToken, err := util.GetInfisicalServiceToken(cmd)
if err != nil { if err != nil {
util.HandleError(err, "Unable to parse flag") util.HandleError(err, "Unable to parse flag")
} }
+2 -1
View File
@@ -36,7 +36,8 @@ var getCmd = &cobra.Command{
} }
} }
infisicalToken, err := cmd.Flags().GetString("token") infisicalToken, err := util.GetInfisicalServiceToken(cmd)
if err != nil { if err != nil {
util.HandleError(err, "Unable to parse flag") util.HandleError(err, "Unable to parse flag")
} }
+2 -1
View File
@@ -62,7 +62,8 @@ var runCmd = &cobra.Command{
} }
} }
infisicalToken, err := cmd.Flags().GetString("token") infisicalToken, err := util.GetInfisicalServiceToken(cmd)
if err != nil { if err != nil {
util.HandleError(err, "Unable to parse flag") util.HandleError(err, "Unable to parse flag")
} }
+7 -6
View File
@@ -7,7 +7,6 @@ import (
"crypto/sha256" "crypto/sha256"
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"os"
"regexp" "regexp"
"sort" "sort"
"strings" "strings"
@@ -39,10 +38,10 @@ var secretsCmd = &cobra.Command{
} }
} }
infisicalToken, err := cmd.Flags().GetString("token") infisicalToken, err := util.GetInfisicalServiceToken(cmd)
if infisicalToken == "" { if err != nil {
infisicalToken = os.Getenv(util.INFISICAL_TOKEN_NAME) util.HandleError(err, "Unable to parse flag")
} }
if err != nil { if err != nil {
@@ -399,7 +398,8 @@ func getSecretsByNames(cmd *cobra.Command, args []string) {
} }
} }
infisicalToken, err := cmd.Flags().GetString("token") infisicalToken, err := util.GetInfisicalServiceToken(cmd)
if err != nil { if err != nil {
util.HandleError(err, "Unable to parse flag") util.HandleError(err, "Unable to parse flag")
} }
@@ -466,7 +466,8 @@ func generateExampleEnv(cmd *cobra.Command, args []string) {
util.HandleError(err, "Unable to parse flag") util.HandleError(err, "Unable to parse flag")
} }
infisicalToken, err := cmd.Flags().GetString("token") infisicalToken, err := util.GetInfisicalServiceToken(cmd)
if err != nil { if err != nil {
util.HandleError(err, "Unable to parse flag") util.HandleError(err, "Unable to parse flag")
} }
+15
View File
@@ -11,6 +11,7 @@ import (
"strings" "strings"
"github.com/Infisical/infisical-merge/packages/models" "github.com/Infisical/infisical-merge/packages/models"
"github.com/spf13/cobra"
) )
type DecodedSymmetricEncryptionDetails = struct { type DecodedSymmetricEncryptionDetails = struct {
@@ -63,6 +64,20 @@ func IsSecretTypeValid(s string) bool {
return false return false
} }
func GetInfisicalServiceToken(cmd *cobra.Command) (serviceToken string, err error) {
infisicalToken, err := cmd.Flags().GetString("token")
if infisicalToken == "" {
infisicalToken = os.Getenv(INFISICAL_TOKEN_NAME)
}
if err != nil {
return "", err
}
return infisicalToken, nil
}
// Checks if the passed in email already exists in the users slice // Checks if the passed in email already exists in the users slice
func ConfigContainsEmail(users []models.LoggedInUser, email string) bool { func ConfigContainsEmail(users []models.LoggedInUser, email string) bool {
for _, value := range users { for _, value := range users {
+29
View File
@@ -126,9 +126,38 @@ services:
ports: ports:
- 1025:1025 # SMTP server - 1025:1025 # SMTP server
- 8025:8025 # Web UI - 8025:8025 # Web UI
openldap: # note: more advanced configuration is available
image: osixia/openldap:1.5.0
restart: always
environment:
LDAP_ORGANISATION: Acme
LDAP_DOMAIN: acme.com
LDAP_ADMIN_PASSWORD: admin
ports:
- 389:389
- 636:636
volumes:
- ldap_data:/var/lib/ldap
- ldap_config:/etc/ldap/slapd.d
profiles: [ldap]
phpldapadmin: # username: cn=admin,dc=acme,dc=com, pass is admin
image: osixia/phpldapadmin:latest
restart: always
environment:
- PHPLDAPADMIN_LDAP_HOSTS=openldap
- PHPLDAPADMIN_HTTPS=false
ports:
- 6433:80
depends_on:
- openldap
profiles: [ldap]
volumes: volumes:
postgres-data: postgres-data:
driver: local driver: local
redis_data: redis_data:
driver: local driver: local
ldap_data:
ldap_config:
+1 -1
View File
@@ -10,7 +10,7 @@ description: "Log in to Infisical with LDAP"
then you should contact [email protected] to purchase an enterprise license to use it. then you should contact [email protected] to purchase an enterprise license to use it.
</Info> </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); this includes support for Active Directory. 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> <Steps>
<Step title="Prepare the LDAP configuration in Infisical"> <Step title="Prepare the LDAP configuration in Infisical">
@@ -5,7 +5,6 @@ description: "Log in to Infisical with LDAP"
<Info> <Info>
LDAP is a paid feature. 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, If you're using Infisical Cloud, then it is available under the **Enterprise Tier**. If you're self-hosting Infisical,
then you should contact [email protected] to purchase an enterprise license to use it. then you should contact [email protected] to purchase an enterprise license to use it.
</Info> </Info>
@@ -30,7 +29,6 @@ You can configure your organization in Infisical to have members authenticate wi
</Step> </Step>
<Step title="Enable LDAP in Infisical"> <Step title="Enable LDAP in Infisical">
Enabling LDAP allows members in your organization to log into Infisical via LDAP. Enabling LDAP allows members in your organization to log into Infisical via LDAP.
![LDAP toggle](/images/platform/ldap/ldap-toggle.png) ![LDAP toggle](/images/platform/ldap/ldap-toggle.png)
</Step> </Step>
</Steps> </Steps>
@@ -5,7 +5,6 @@ description: "Configure JumpCloud LDAP for Logging into Infisical"
<Info> <Info>
LDAP is a paid feature. 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, If you're using Infisical Cloud, then it is available under the **Enterprise Tier**. If you're self-hosting Infisical,
then you should contact [email protected] to purchase an enterprise license to use it. then you should contact [email protected] to purchase an enterprise license to use it.
</Info> </Info>
@@ -47,7 +46,6 @@ description: "Configure JumpCloud LDAP for Logging into Infisical"
</Step> </Step>
<Step title="Enable LDAP in Infisical"> <Step title="Enable LDAP in Infisical">
Enabling LDAP allows members in your organization to log into Infisical via LDAP. Enabling LDAP allows members in your organization to log into Infisical via LDAP.
![LDAP toggle](/images/platform/ldap/ldap-toggle.png) ![LDAP toggle](/images/platform/ldap/ldap-toggle.png)
</Step> </Step>
</Steps> </Steps>
+1 -1
View File
@@ -4,7 +4,7 @@ description: "Configure Azure SAML for Infisical SSO"
--- ---
<Info> <Info>
Azure SAML SSO feature is a paid feature. Azure SAML SSO is a paid feature.
If you're using Infisical Cloud, then it is available under the **Pro Tier**. If you're self-hosting Infisical, If you're using Infisical Cloud, then it is available under the **Pro Tier**. If you're self-hosting Infisical,
then you should contact [email protected] to purchase an enterprise license to use it. then you should contact [email protected] to purchase an enterprise license to use it.
@@ -4,7 +4,7 @@ description: "Configure JumpCloud SAML for Infisical SSO"
--- ---
<Info> <Info>
JumpCloud SAML SSO feature is a paid feature. JumpCloud SAML SSO is a paid feature.
If you're using Infisical Cloud, then it is available under the **Pro Tier**. If you're self-hosting Infisical, If you're using Infisical Cloud, then it is available under the **Pro Tier**. If you're self-hosting Infisical,
then you should contact [email protected] to purchase an enterprise license to use it. then you should contact [email protected] to purchase an enterprise license to use it.
+1 -1
View File
@@ -4,7 +4,7 @@ description: "Configure Okta SAML 2.0 for Infisical SSO"
--- ---
<Info> <Info>
Okta SAML SSO feature is a paid feature. Okta SAML SSO is a paid feature.
If you're using Infisical Cloud, then it is available under the **Pro Tier**. If you're self-hosting Infisical, If you're using Infisical Cloud, then it is available under the **Pro Tier**. If you're self-hosting Infisical,
then you should contact [email protected] to purchase an enterprise license to use it. then you should contact [email protected] to purchase an enterprise license to use it.
+1
View File
@@ -1,5 +1,6 @@
--- ---
title: "Overview" title: "Overview"
description: "This page describes how to manage secrets using Infisical Agent."
--- ---
Infisical Agent is a client daemon that simplifies the adoption of Infisical by providing a more scalable and user-friendly approach for applications to interact with Infisical. Infisical Agent is a client daemon that simplifies the adoption of Infisical by providing a more scalable and user-friendly approach for applications to interact with Infisical.
+8
View File
@@ -158,6 +158,14 @@
"documentation/platform/ldap/general" "documentation/platform/ldap/general"
] ]
}, },
{
"group": "LDAP",
"pages": [
"documentation/platform/ldap/overview",
"documentation/platform/ldap/jumpcloud",
"documentation/platform/ldap/general"
]
},
{ {
"group": "SCIM", "group": "SCIM",
"pages": [ "pages": [
@@ -1,401 +0,0 @@
import { useEffect, useState } from "react";
import { useRouter } from "next/router";
import { faEye, faEyeSlash, faPenToSquare, faPlus, faX } from "@fortawesome/free-solid-svg-icons";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { Select, SelectItem } from "@app/components/v2";
import { useSubscription, useWorkspace } from "@app/context";
import updateUserProjectPermission from "@app/ee/api/memberships/UpdateUserProjectPermission";
import {
useDeleteUserFromWorkspace,
useGetUserWsKey,
useUpdateUserWorkspaceRole,
useUploadWsKey
} from "@app/hooks/api";
import { decryptAssymmetric, encryptAssymmetric } from "../../utilities/cryptography/crypto";
import guidGenerator from "../../utilities/randomId";
import Button from "../buttons/Button";
import UpgradePlanModal from "../dialog/UpgradePlan";
// const roles = ['admin', 'user'];
// TODO: Set type for this
type Props = {
userData: any[];
changeData: (users: any[]) => void;
myUser: string;
filter: string;
isUserListLoading: boolean;
};
type EnvironmentProps = {
name: string;
slug: string;
};
/**
* This is the component that shows the users of a certin project
* #TODO: add the possibility of choosing and doing operations on multiple users.
* @param {*} props
* @returns
*/
const ProjectUsersTable = ({ userData, changeData, myUser, filter, isUserListLoading }: Props) => {
const { currentWorkspace } = useWorkspace();
const { subscription } = useSubscription();
const { data: wsKey } = useGetUserWsKey(currentWorkspace?.id ?? "");
const { mutateAsync: deleteUserFromWorkspaceMutateAsync } = useDeleteUserFromWorkspace();
const { mutateAsync: uploadWsKeyMutateAsync } = useUploadWsKey();
const { mutateAsync: updateUserWorkspaceRoleMutateAsync } = useUpdateUserWorkspaceRole();
// const [roleSelected, setRoleSelected] = useState(
// Array(userData?.length).fill(userData.map((user) => user.role))
// );
const router = useRouter();
const [myRole, setMyRole] = useState("member");
const [workspaceEnvs, setWorkspaceEnvs] = useState<EnvironmentProps[]>([]);
const [isUpgradeModalOpen, setIsUpgradeModalOpen] = useState(false);
const { createNotification } = useNotificationContext();
const workspaceId = router.query.id as string;
// Delete the row in the table (e.g. a user)
// #TODO: Add a pop-up that warns you that the user is going to be deleted.
const handleDelete = async (email: string) => {
await deleteUserFromWorkspaceMutateAsync({ emails: [email], workspaceId });
};
const handleRoleUpdate = async (index: number, e: string) => {
await updateUserWorkspaceRoleMutateAsync({
workspaceId,
membershipId: userData[index].membershipId,
role: e.toLowerCase()
});
createNotification({
text: "Successfully changed user role.",
type: "success"
});
};
const handlePermissionUpdate = (
index: number,
val: string,
membershipId: string,
slug: string
) => {
let denials: { ability: string; environmentSlug: string }[];
if (val === "Read Only") {
denials = [
{
ability: "write",
environmentSlug: slug
}
];
} else if (val === "No Access") {
denials = [
{
ability: "write",
environmentSlug: slug
},
{
ability: "read",
environmentSlug: slug
}
];
} else if (val === "Add Only") {
denials = [
{
ability: "read",
environmentSlug: slug
}
];
} else {
denials = [];
}
if (subscription?.rbac === false) {
setIsUpgradeModalOpen(true);
} else {
const allDenials = userData[index].deniedPermissions
.filter(
(perm: { ability: string; environmentSlug: string }) => perm.environmentSlug !== slug
)
.concat(denials);
updateUserProjectPermission({ membershipId, denials: allDenials });
changeData([
...userData.slice(0, index),
...[
{
key: userData[index].key,
firstName: userData[index].firstName,
lastName: userData[index].lastName,
email: userData[index].email,
role: userData[index].role,
status: userData[index].status,
userId: userData[index].userId,
membershipId: userData[index].membershipId,
publicKey: userData[index].publicKey,
deniedPermissions: allDenials
}
],
...userData.slice(index + 1, userData?.length)
]);
createNotification({
text: "Successfully changed user permissions.",
type: "success"
});
}
};
useEffect(() => {
setMyRole(userData.filter((user) => user.email === myUser)[0]?.role);
(async () => {
if (currentWorkspace) {
setWorkspaceEnvs(currentWorkspace.environments);
}
})();
}, [userData, myUser, currentWorkspace]);
const grantAccess = async (id: string, publicKey: string) => {
if (wsKey) {
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
// assymmetrically decrypt symmetric key with local private key
const key = decryptAssymmetric({
ciphertext: wsKey.encryptedKey,
nonce: wsKey.nonce,
publicKey: wsKey.sender.publicKey,
privateKey: PRIVATE_KEY
});
const { ciphertext, nonce } = encryptAssymmetric({
plaintext: key,
publicKey,
privateKey: PRIVATE_KEY
});
await uploadWsKeyMutateAsync({
workspaceId,
userId: id,
encryptedKey: ciphertext,
nonce
});
router.reload();
}
};
const closeUpgradeModal = () => {
setIsUpgradeModalOpen(false);
};
return (
<div className="table-container relative mb-6 mt-1 min-w-max rounded-md border border-mineshaft-600 bg-bunker">
<div className="absolute h-[3.1rem] w-full rounded-t-md bg-white/5" />
{subscription && (
<UpgradePlanModal
isOpen={isUpgradeModalOpen}
onClose={closeUpgradeModal}
text={
subscription.slug === null
? "You can use RBAC under an Enterprise license"
: "You can use RBAC if you switch to Infisical's Team Plan."
}
/>
)}
<table className="my-0.5 w-full">
<thead className="bg-mineshaft-800 text-xs font-light text-gray-400">
<tr>
<th className="py-3.5 pl-4 text-left">NAME</th>
<th className="py-3.5 pl-4 text-left">EMAIL</th>
<th className="py-3.5 pl-6 pr-10 text-left">ROLE</th>
{workspaceEnvs.map((env) => (
<th key={guidGenerator()} className="max-w-min break-normal py-1 pl-2 text-left">
<span>
{env.slug.toUpperCase()}
<br />
</span>
{/* <span>PERMISSION</span> */}
</th>
))}
<th aria-label="buttons" />
</tr>
</thead>
<tbody>
{!isUserListLoading &&
userData?.filter(
(user) =>
user.firstName?.toLowerCase().includes(filter) ||
user.lastName?.toLowerCase().includes(filter) ||
user.email?.toLowerCase().includes(filter)
).length > 0 &&
userData
?.filter(
(user) =>
user.firstName?.toLowerCase().includes(filter) ||
user.lastName?.toLowerCase().includes(filter) ||
user.email?.toLowerCase().includes(filter)
)
.map((row, index) => (
<tr key={guidGenerator()} className="bg-mineshaft-800 text-sm">
<td className="border-t border-mineshaft-600 py-2 pl-4 text-gray-300">
{row.firstName} {row.lastName}
</td>
<td className="border-t border-mineshaft-600 py-2 pl-4 text-gray-300">
{row.email}
</td>
<td className="border-t border-mineshaft-600 py-2 pl-6 pr-10 text-gray-300">
<div className="flex h-full flex-row items-center justify-start">
<Select
className="w-36 bg-mineshaft-700"
dropdownContainerClassName="bg-mineshaft-700"
// open={isOpen}
onValueChange={(e) => handleRoleUpdate(index, e)}
value={row.role}
isDisabled={myRole !== "admin" || myUser === row.email}
// onOpenChange={(open) => setIsOpen(open)}
>
<SelectItem value="admin">Admin</SelectItem>
<SelectItem value="member">Member</SelectItem>
</Select>
{row.status === "completed" && myUser !== row.email && (
<div className="rounded-md border border-mineshaft-700 bg-white/5 text-white duration-200 hover:bg-primary hover:text-black">
<Button
onButtonPressed={() => grantAccess(row.userId, row.publicKey)}
color="mineshaft"
text="Grant Access"
size="md"
/>
</div>
)}
</div>
</td>
{workspaceEnvs.map((env) => (
<td
key={guidGenerator()}
className="border-t border-mineshaft-700 py-2 pl-2 text-gray-300"
>
<Select
className="w-16 bg-mineshaft-700"
dropdownContainerClassName="bg-mineshaft-800 border border-mineshaft-600 text-bunker-200"
position="item-aligned"
// open={isOpen}
onValueChange={(val) =>
handlePermissionUpdate(index, val, row.membershipId, env.slug)
}
value={
// eslint-disable-next-line no-nested-ternary
row.deniedPermissions
.filter((perm: any) => perm.environmentSlug === env.slug)
.map((perm: { ability: string }) => perm.ability)
.includes("write") &&
row.deniedPermissions
.filter((perm: any) => perm.environmentSlug === env.slug)
.map((perm: { ability: string }) => perm.ability)
.includes("read")
? "No Access"
: // eslint-disable-next-line no-nested-ternary
row.deniedPermissions
.filter((perm: any) => perm.environmentSlug === env.slug)
.map((perm: { ability: string }) => perm.ability)
.includes("write") &&
!row.deniedPermissions
.filter((perm: any) => perm.environmentSlug === env.slug)
.map((perm: { ability: string }) => perm.ability)
.includes("read")
? "Read Only"
: !row.deniedPermissions
.filter((perm: any) => perm.environmentSlug === env.slug)
.map((perm: { ability: string }) => perm.ability)
.includes("write") &&
row.deniedPermissions
.filter((perm: any) => perm.environmentSlug === env.slug)
.map((perm: { ability: string }) => perm.ability)
.includes("read")
? "Add Only"
: "Read & Write"
}
icon={
// eslint-disable-next-line no-nested-ternary
row.deniedPermissions
.filter((perm: any) => perm.environmentSlug === env.slug)
.map((perm: { ability: string }) => perm.ability)
.includes("write") &&
row.deniedPermissions
.filter((perm: any) => perm.environmentSlug === env.slug)
.map((perm: { ability: string }) => perm.ability)
.includes("read")
? faEyeSlash
: // eslint-disable-next-line no-nested-ternary
row.deniedPermissions
.filter((perm: any) => perm.environmentSlug === env.slug)
.map((perm: { ability: string }) => perm.ability)
.includes("write") &&
!row.deniedPermissions
.filter((perm: any) => perm.environmentSlug === env.slug)
.map((perm: { ability: string }) => perm.ability)
.includes("read")
? faEye
: !row.deniedPermissions
.filter((perm: any) => perm.environmentSlug === env.slug)
.map((perm: { ability: string }) => perm.ability)
.includes("write") &&
row.deniedPermissions
.filter((perm: any) => perm.environmentSlug === env.slug)
.map((perm: { ability: string }) => perm.ability)
.includes("read")
? faPlus
: faPenToSquare
}
isDisabled={myRole !== "admin"}
// onOpenChange={(open) => setIsOpen(open)}
>
<SelectItem value="No Access" customIcon={faEyeSlash}>
No Access
</SelectItem>
<SelectItem value="Read Only" customIcon={faEye}>
Read Only
</SelectItem>
<SelectItem value="Add Only" customIcon={faPlus}>
Add Only
</SelectItem>
<SelectItem value="Read & Write" customIcon={faPenToSquare}>
Read & Write
</SelectItem>
</Select>
</td>
))}
<td className="border-0.5 flex flex-row justify-end border-t border-mineshaft-700 py-2 pl-8 pr-8">
{myUser !== row.email &&
// row.role !== "admin" &&
myRole !== "member" ? (
<div className="mt-0.5 flex items-center opacity-50 hover:opacity-100">
<Button
onButtonPressed={() => handleDelete(row.email)}
color="red"
size="icon-sm"
icon={faX}
/>
</div>
) : (
<div className="h-9 w-9" />
)}
</td>
</tr>
))}
{isUserListLoading && (
<>
<tr
key={guidGenerator()}
className="h-14 w-full animate-pulse bg-mineshaft-800 text-sm"
/>
<tr
key={guidGenerator()}
className="h-14 w-full animate-pulse bg-mineshaft-800 text-sm"
/>
</>
)}
</tbody>
</table>
</div>
);
};
export default ProjectUsersTable;
@@ -15,6 +15,7 @@ export enum OrgPermissionSubjects {
IncidentAccount = "incident-contact", IncidentAccount = "incident-contact",
Scim = "scim", Scim = "scim",
Sso = "sso", Sso = "sso",
Ldap = "ldap",
Billing = "billing", Billing = "billing",
SecretScanning = "secret-scanning", SecretScanning = "secret-scanning",
Identity = "identity" Identity = "identity"
@@ -29,6 +30,7 @@ export type OrgPermissionSet =
| [OrgPermissionActions, OrgPermissionSubjects.IncidentAccount] | [OrgPermissionActions, OrgPermissionSubjects.IncidentAccount]
| [OrgPermissionActions, OrgPermissionSubjects.Scim] | [OrgPermissionActions, OrgPermissionSubjects.Scim]
| [OrgPermissionActions, OrgPermissionSubjects.Sso] | [OrgPermissionActions, OrgPermissionSubjects.Sso]
| [OrgPermissionActions, OrgPermissionSubjects.Ldap]
| [OrgPermissionActions, OrgPermissionSubjects.SecretScanning] | [OrgPermissionActions, OrgPermissionSubjects.SecretScanning]
| [OrgPermissionActions, OrgPermissionSubjects.Billing] | [OrgPermissionActions, OrgPermissionSubjects.Billing]
| [OrgPermissionActions, OrgPermissionSubjects.Identity]; | [OrgPermissionActions, OrgPermissionSubjects.Identity];
+7
View File
@@ -14,6 +14,8 @@ import {
Login1Res, Login1Res,
Login2DTO, Login2DTO,
Login2Res, Login2Res,
LoginLDAPDTO,
LoginLDAPRes,
ResetPasswordDTO, ResetPasswordDTO,
SendMfaTokenDTO, SendMfaTokenDTO,
SRP1DTO, SRP1DTO,
@@ -37,6 +39,11 @@ export const login2 = async (loginDetails: Login2DTO) => {
return data; return data;
}; };
export const loginLDAPRedirect = async (loginLDAPDetails: LoginLDAPDTO) => {
const { data } = await apiRequest.post<LoginLDAPRes>("/api/v1/ldap/login", loginLDAPDetails); // return if account is complete or not + provider auth token
return data;
}
export const useLogin1 = () => { export const useLogin1 = () => {
return useMutation({ return useMutation({
mutationFn: async (details: { mutationFn: async (details: {
+10
View File
@@ -53,6 +53,16 @@ export type Login2Res = {
tag?: string; tag?: string;
} }
export type LoginLDAPDTO = {
organizationSlug: string;
username: string;
password: string;
}
export type LoginLDAPRes = {
nextUrl: string;
}
export type SRP1DTO = { export type SRP1DTO = {
clientPublicKey: string; clientPublicKey: string;
} }
+1
View File
@@ -8,6 +8,7 @@ export * from "./incidentContacts";
export * from "./integrationAuth"; export * from "./integrationAuth";
export * from "./integrations"; export * from "./integrations";
export * from "./keys"; export * from "./keys";
export * from "./ldapConfig";
export * from "./organization"; export * from "./organization";
export * from "./roles"; export * from "./roles";
export * from "./scim"; export * from "./scim";
@@ -0,0 +1,5 @@
export {
useCreateLDAPConfig,
useGetLDAPConfig,
useUpdateLDAPConfig
} from "./queries";
@@ -0,0 +1,103 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
const ldapConfigKeys = {
getLDAPConfig: (orgId: string) => [{ orgId }, "organization-ldap"] as const,
}
export const useGetLDAPConfig = (organizationId: string) => {
return useQuery({
queryKey: ldapConfigKeys.getLDAPConfig(organizationId),
queryFn: async () => {
const { data } = await apiRequest.get(
`/api/v1/ldap/config?organizationId=${organizationId}`
);
return data;
},
enabled: true
});
}
export const useCreateLDAPConfig = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({
organizationId,
isActive,
url,
bindDN,
bindPass,
searchBase,
caCert
}: {
organizationId: string;
isActive: boolean;
url: string;
bindDN: string;
bindPass: string;
searchBase: string;
caCert?: string;
}) => {
const { data } = await apiRequest.post(
"/api/v1/ldap/config",
{
organizationId,
isActive,
url,
bindDN,
bindPass,
searchBase,
caCert
}
);
return data;
},
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));
}
});
};
@@ -19,6 +19,7 @@ export type SubscriptionPlan = {
environmentLimit: number; environmentLimit: number;
samlSSO: boolean; samlSSO: boolean;
scim: boolean; scim: boolean;
ldap: boolean;
status: status:
| "incomplete" | "incomplete"
| "incomplete_expired" | "incomplete_expired"
+2 -2
View File
@@ -50,9 +50,9 @@ export const useAddUserToWsNonE2EE = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation<{}, {}, AddUserToWsDTONonE2EE>({ return useMutation<{}, {}, AddUserToWsDTONonE2EE>({
mutationFn: async ({ projectId, emails }) => { mutationFn: async ({ projectId, usernames }) => {
const { data } = await apiRequest.post(`/api/v2/workspace/${projectId}/memberships`, { const { data } = await apiRequest.post(`/api/v2/workspace/${projectId}/memberships`, {
emails usernames
}); });
return data; return data;
}, },
+7 -4
View File
@@ -7,13 +7,15 @@ export enum AuthMethod {
GITLAB = "gitlab", GITLAB = "gitlab",
OKTA_SAML = "okta-saml", OKTA_SAML = "okta-saml",
AZURE_SAML = "azure-saml", AZURE_SAML = "azure-saml",
JUMPCLOUD_SAML = "jumpcloud-saml" JUMPCLOUD_SAML = "jumpcloud-saml",
LDAP = "ldap"
} }
export type User = { export type User = {
createdAt: Date; createdAt: Date;
updatedAt: Date; updatedAt: Date;
email: string; username: string;
email?: string;
superAdmin: boolean; superAdmin: boolean;
firstName?: string; firstName?: string;
lastName?: string; lastName?: string;
@@ -38,7 +40,8 @@ export type UserEnc = {
export type OrgUser = { export type OrgUser = {
id: string; id: string;
user: { user: {
email: string; username: string;
email?: string;
firstName: string; firstName: string;
lastName: string; lastName: string;
id: string; id: string;
@@ -75,7 +78,7 @@ export type AddUserToWsDTOE2EE = {
export type AddUserToWsDTONonE2EE = { export type AddUserToWsDTONonE2EE = {
projectId: string; projectId: string;
emails: string[]; usernames: string[];
}; };
export type UpdateOrgUserRoleDTO = { export type UpdateOrgUserRoleDTO = {
+2 -2
View File
@@ -323,11 +323,11 @@ export const useDeleteUserFromWorkspace = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: async ({ emails, workspaceId }: { workspaceId: string; emails: string[] }) => { mutationFn: async ({ usernames, workspaceId }: { workspaceId: string; usernames: string[] }) => {
const { const {
data: { deletedMembership } data: { deletedMembership }
} = await apiRequest.delete(`/api/v2/workspace/${workspaceId}/memberships`, { } = await apiRequest.delete(`/api/v2/workspace/${workspaceId}/memberships`, {
data: { emails } data: { usernames }
}); });
return deletedMembership; return deletedMembership;
}, },
@@ -91,7 +91,7 @@ export const AdminLayout = ({ children }: LayoutProps) => {
console.error(error); console.error(error);
} }
}; };
return ( return (
<> <>
<div className="dark hidden h-screen w-full flex-col overflow-x-hidden md:flex"> <div className="dark hidden h-screen w-full flex-col overflow-x-hidden md:flex">
@@ -121,7 +121,7 @@ export const AdminLayout = ({ children }: LayoutProps) => {
</div> </div>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1"> <DropdownMenuContent align="start" className="p-1">
<div className="px-2 py-1 text-xs text-mineshaft-400">{user?.email}</div> <div className="px-2 py-1 text-xs text-mineshaft-400">{user?.username}</div>
<Link href="/personal-settings"> <Link href="/personal-settings">
<DropdownMenuItem>Personal Settings</DropdownMenuItem> <DropdownMenuItem>Personal Settings</DropdownMenuItem>
</Link> </Link>
+5 -6
View File
@@ -231,11 +231,10 @@ export const AppLayout = ({ children }: LayoutProps) => {
if (addMembers) { if (addMembers) {
const orgUsers = await fetchOrgUsers(currentOrg.id); const orgUsers = await fetchOrgUsers(currentOrg.id);
await addUsersToProject.mutateAsync({ await addUsersToProject.mutateAsync({
emails: orgUsers usernames: orgUsers
.map((member) => member.user.email) .map((member) => member.user.username)
.filter((email) => email !== user.email), .filter((username) => username !== user.username),
projectId: newProjectId projectId: newProjectId
}); });
} }
@@ -292,7 +291,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
</div> </div>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1"> <DropdownMenuContent align="start" className="p-1">
<div className="px-2 py-1 text-xs text-mineshaft-400">{user?.email}</div> <div className="px-2 py-1 text-xs text-mineshaft-400">{user?.username}</div>
{orgs?.map((org) => { {orgs?.map((org) => {
return ( return (
<DropdownMenuItem key={org.id}> <DropdownMenuItem key={org.id}>
@@ -367,7 +366,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
</div> </div>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1"> <DropdownMenuContent align="start" className="p-1">
<div className="px-2 py-1 text-xs text-mineshaft-400">{user?.email}</div> <div className="px-2 py-1 text-xs text-mineshaft-400">{user?.username}</div>
<Link href="/personal-settings"> <Link href="/personal-settings">
<DropdownMenuItem>Personal Settings</DropdownMenuItem> <DropdownMenuItem>Personal Settings</DropdownMenuItem>
</Link> </Link>
@@ -513,9 +513,9 @@ const OrganizationPage = withPermission(
const orgUsers = await fetchOrgUsers(currentOrg); const orgUsers = await fetchOrgUsers(currentOrg);
await addUsersToProject.mutateAsync({ await addUsersToProject.mutateAsync({
emails: orgUsers usernames: orgUsers
.map((member) => member.user.email) .map((member) => member.user.username)
.filter((email) => email !== user.email), .filter((username) => username !== user.username),
projectId: newProjectId projectId: newProjectId
}); });
} }
+6 -3
View File
@@ -7,9 +7,9 @@ import { getAuthToken, isLoggedIn } from "@app/reactQuery";
import { import {
InitialStep, InitialStep,
LDAPStep,
MFAStep, MFAStep,
SAMLSSOStep SAMLSSOStep} from "./components";
} from "./components";
import { navigateUserToOrg } from "./Login.utils"; import { navigateUserToOrg } from "./Login.utils";
export const Login = () => { export const Login = () => {
@@ -72,7 +72,10 @@ export const Login = () => {
return ( return (
<SAMLSSOStep setStep={setStep} /> <SAMLSSOStep setStep={setStep} />
); );
case 3:
return (
<LDAPStep setStep={setStep} />
);
default: default:
return <div />; return <div />;
} }
+3 -3
View File
@@ -15,7 +15,7 @@ export const LoginSSO = ({ providerAuthToken }: Props) => {
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
const { const {
email, username,
isUserCompleted isUserCompleted
} = jwt_decode(providerAuthToken) as any; } = jwt_decode(providerAuthToken) as any;
@@ -35,7 +35,7 @@ export const LoginSSO = ({ providerAuthToken }: Props) => {
return ( return (
<PasswordStep <PasswordStep
providerAuthToken={providerAuthToken} providerAuthToken={providerAuthToken}
email={email} email={username}
password={password} password={password}
setPassword={setPassword} setPassword={setPassword}
setStep={setStep} setStep={setStep}
@@ -45,7 +45,7 @@ export const LoginSSO = ({ providerAuthToken }: Props) => {
return ( return (
<MFAStep <MFAStep
providerAuthToken={providerAuthToken} providerAuthToken={providerAuthToken}
email={email} email={username}
password={password} password={password}
/> />
); );
@@ -180,7 +180,20 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
leftIcon={<FontAwesomeIcon icon={faLock} className="mr-2" />} leftIcon={<FontAwesomeIcon icon={faLock} className="mr-2" />}
className="mx-0 h-10 w-full" className="mx-0 h-10 w-full"
> >
Continue with SAML SSO Continue with SAML
</Button>
</div>
<div className="mt-2 w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
<Button
colorSchema="primary"
variant="outline_bg"
onClick={() => {
setStep(3);
}}
leftIcon={<FontAwesomeIcon icon={faLock} className="mr-2" />}
className="mx-0 h-10 w-full"
>
Continue with LDAP
</Button> </Button>
</div> </div>
<div className="my-4 flex w-1/4 min-w-[20rem] flex-row items-center py-2 lg:w-1/6"> <div className="my-4 flex w-1/4 min-w-[20rem] flex-row items-center py-2 lg:w-1/6">
@@ -0,0 +1,137 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { Button, Input } from "@app/components/v2";
import { loginLDAPRedirect } from "@app/hooks/api/auth/queries";
type Props = {
setStep: (step: number) => void;
}
export const LDAPStep = ({
setStep
}: Props) => {
const { createNotification } = useNotificationContext();
const [organizationSlug, setOrganizationSlug] = useState("");
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const { t } = useTranslation();
// const queryParams = new URLSearchParams(window.location.search);
const handleSubmission = async (e:React.FormEvent) => {
e.preventDefault()
try {
const { nextUrl } = await loginLDAPRedirect({
organizationSlug,
username,
password
});
if (!nextUrl) {
createNotification({
text: "Login unsuccessful. Double-check your credentials and try again.",
type: "error"
});
return;
}
createNotification({
text: "Successfully logged in",
type: "success"
});
// redirects either to /login/sso or /signup/sso
window.open(nextUrl);
window.close();
} catch (err) {
createNotification({
text: "Login unsuccessful. Double-check your credentials and try again.",
type: "error"
});
}
// TODO: add callback port support
// const callbackPort = queryParams.get("callback_port");
// window.open(`/api/v1/ldap/redirect/saml2/${ssoIdentifier}${callbackPort ? `?callback_port=${callbackPort}` : ""}`);
// window.close();
}
return (
<div className="mx-auto w-full max-w-md md:px-6">
<p className="mx-auto mb-6 flex w-max justify-center text-xl font-medium text-transparent bg-clip-text bg-gradient-to-b from-white to-bunker-200 text-center mb-8">
What&apos;s your LDAP Login?
</p>
<form onSubmit={handleSubmission}>
<div className="relative flex items-center justify-center lg:w-1/6 w-1/4 min-w-[20rem] md:min-w-[22rem] mx-auto w-full rounded-lg max-h-24 md:max-h-28">
<div className="flex items-center justify-center w-full rounded-lg max-h-24 md:max-h-28">
<Input
value={organizationSlug}
onChange={(e) => setOrganizationSlug(e.target.value)}
type="text"
placeholder="Enter your organization slug..."
isRequired
autoComplete="email"
id="email"
className="h-12"
/>
</div>
</div>
<div className="mt-2 relative flex items-center justify-center lg:w-1/6 w-1/4 min-w-[20rem] md:min-w-[22rem] mx-auto w-full rounded-lg max-h-24 md:max-h-28">
<div className="flex items-center justify-center w-full rounded-lg max-h-24 md:max-h-28">
<Input
value={username}
onChange={(e) => setUsername(e.target.value)}
type="text"
placeholder="Enter your LDAP username..."
isRequired
autoComplete="email"
id="email"
className="h-12"
/>
</div>
</div>
<div className="mt-2 relative flex items-center justify-center lg:w-1/6 w-1/4 min-w-[20rem] md:min-w-[22rem] mx-auto w-full rounded-lg max-h-24 md:max-h-28">
<div className="flex items-center justify-center w-full rounded-lg max-h-24 md:max-h-28">
<Input
value={password}
onChange={(e) => setPassword(e.target.value)}
type="password"
placeholder="Enter your LDAP password..."
isRequired
autoComplete="current-password"
id="current-password"
className="select:-webkit-autofill:focus h-10"
/>
</div>
</div>
<div className='lg:w-1/6 w-1/4 w-full mx-auto flex items-center justify-center min-w-[20rem] md:min-w-[22rem] text-center rounded-md mt-4'>
<Button
type="submit"
colorSchema="primary"
variant="outline_bg"
isFullWidth
className="h-14"
>
{t("login.login")}
</Button>
</div>
</form>
<div className="flex flex-row items-center justify-center mt-4">
<button
onClick={() => {
setStep(0);
}}
type="button"
className="text-bunker-300 text-sm hover:underline mt-2 hover:underline-offset-4 hover:decoration-primary-700 hover:text-bunker-200 duration-200 cursor-pointer"
>
{t("login.other-option")}
</button>
</div>
</div>
);
};
@@ -0,0 +1 @@
export { LDAPStep } from "./LDAPStep";
@@ -1,4 +1,5 @@
export { InitialStep } from "./InitialStep"; export { InitialStep } from "./InitialStep";
export { LDAPStep } from "./LDAPStep";
export { MFAStep } from "./MFAStep"; export { MFAStep } from "./MFAStep";
export { SAMLSSOStep } from "./SAMLSSOStep"; export { SAMLSSOStep } from "./SAMLSSOStep";
@@ -113,8 +113,8 @@ export const OrgMembersSection = () => {
/> />
<DeleteActionModal <DeleteActionModal
isOpen={popUp.removeMember.isOpen} isOpen={popUp.removeMember.isOpen}
title={`Are you sure want to remove member with email ${ title={`Are you sure want to remove member with username ${
(popUp?.removeMember?.data as { email: string })?.email || "" (popUp?.removeMember?.data as { username: string })?.username || ""
}?`} }?`}
onChange={(isOpen) => handlePopUpToggle("removeMember", isOpen)} onChange={(isOpen) => handlePopUpToggle("removeMember", isOpen)}
deleteKey="confirm" deleteKey="confirm"
@@ -41,7 +41,7 @@ type Props = {
popUpName: keyof UsePopUpState<["removeMember", "upgradePlan"]>, popUpName: keyof UsePopUpState<["removeMember", "upgradePlan"]>,
data?: { data?: {
orgMembershipId?: string; orgMembershipId?: string;
email?: string; username?: string;
description?: string; description?: string;
} }
) => void; ) => void;
@@ -143,6 +143,7 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLink }: Prop
({ user: u, inviteEmail }) => ({ user: u, inviteEmail }) =>
u?.firstName?.toLowerCase().includes(searchMemberFilter.toLowerCase()) || u?.firstName?.toLowerCase().includes(searchMemberFilter.toLowerCase()) ||
u?.lastName?.toLowerCase().includes(searchMemberFilter.toLowerCase()) || u?.lastName?.toLowerCase().includes(searchMemberFilter.toLowerCase()) ||
u?.username?.toLowerCase().includes(searchMemberFilter.toLowerCase()) ||
u?.email?.toLowerCase().includes(searchMemberFilter.toLowerCase()) || u?.email?.toLowerCase().includes(searchMemberFilter.toLowerCase()) ||
inviteEmail?.includes(searchMemberFilter.toLowerCase()) inviteEmail?.includes(searchMemberFilter.toLowerCase())
), ),
@@ -162,7 +163,7 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLink }: Prop
<THead> <THead>
<Tr> <Tr>
<Th>Name</Th> <Th>Name</Th>
<Th>Email</Th> <Th>Username</Th>
<Th>Role</Th> <Th>Role</Th>
<Th className="w-5" /> <Th className="w-5" />
</Tr> </Tr>
@@ -174,10 +175,11 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLink }: Prop
({ user: u, inviteEmail, role, roleId, id: orgMembershipId, status }) => { ({ user: u, inviteEmail, role, roleId, id: orgMembershipId, status }) => {
const name = u && u.firstName ? `${u.firstName} ${u.lastName}` : "-"; const name = u && u.firstName ? `${u.firstName} ${u.lastName}` : "-";
const email = u?.email || inviteEmail; const email = u?.email || inviteEmail;
const username = u?.username ?? inviteEmail ?? "-";
return ( return (
<Tr key={`org-membership-${orgMembershipId}`} className="w-full"> <Tr key={`org-membership-${orgMembershipId}`} className="w-full">
<Td>{name}</Td> <Td>{name}</Td>
<Td>{email}</Td> <Td>{username}</Td>
<Td> <Td>
<OrgPermissionCan <OrgPermissionCan
I={OrgPermissionActions.Edit} I={OrgPermissionActions.Edit}
@@ -206,7 +208,7 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLink }: Prop
))} ))}
</Select> </Select>
)} )}
{(status === "invited" || status === "verified") && {(status === "invited" || status === "verified") && email &&
serverDetails?.emailConfigured && ( serverDetails?.emailConfigured && (
<Button <Button
isDisabled={!isAllowed} isDisabled={!isAllowed}
@@ -239,7 +241,7 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLink }: Prop
return; return;
} }
handlePopUpOpen("removeMember", { orgMembershipId, email }); handlePopUpOpen("removeMember", { orgMembershipId, username });
}} }}
size="lg" size="lg"
colorSchema="danger" colorSchema="danger"
@@ -82,6 +82,12 @@ const SIMPLE_PERMISSION_OPTIONS = [
icon: faSignIn, icon: faSignIn,
formName: "sso" formName: "sso"
}, },
{
title: "LDAP",
subtitle: "Define organization level LDAP requirements",
icon: faSignIn,
formName: "ldap"
},
{ {
title: "SCIM", title: "SCIM",
subtitle: "Define organization level SCIM requirements", subtitle: "Define organization level SCIM requirements",
@@ -35,6 +35,7 @@ export const formSchema = z.object({
"secret-scanning": generalPermissionSchema, "secret-scanning": generalPermissionSchema,
sso: generalPermissionSchema, sso: generalPermissionSchema,
scim: generalPermissionSchema, scim: generalPermissionSchema,
ldap: generalPermissionSchema,
billing: generalPermissionSchema, billing: generalPermissionSchema,
identity: generalPermissionSchema identity: generalPermissionSchema
}) })
@@ -9,10 +9,6 @@ import { z } from "zod";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { ProjectPermissionCan } from "@app/components/permissions"; import { ProjectPermissionCan } from "@app/components/permissions";
import {
decryptAssymmetric,
encryptAssymmetric
} from "@app/components/utilities/cryptography/crypto";
import { import {
Button, Button,
DeleteActionModal, DeleteActionModal,
@@ -51,8 +47,7 @@ import {
useGetProjectRoles, useGetProjectRoles,
useGetUserWsKey, useGetUserWsKey,
useGetWorkspaceUsers, useGetWorkspaceUsers,
useUpdateUserWorkspaceRole, useUpdateUserWorkspaceRole
useUploadWsKey
} from "@app/hooks/api"; } from "@app/hooks/api";
import { ProjectMembershipRole } from "@app/hooks/api/roles/types"; import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
import { ProjectVersion } from "@app/hooks/api/workspace/types"; import { ProjectVersion } from "@app/hooks/api/workspace/types";
@@ -81,7 +76,7 @@ export const MemberListTab = () => {
const { data: wsKey } = useGetUserWsKey(workspaceId); const { data: wsKey } = useGetUserWsKey(workspaceId);
const { data: members, isLoading: isMembersLoading } = useGetWorkspaceUsers(workspaceId); const { data: members, isLoading: isMembersLoading } = useGetWorkspaceUsers(workspaceId);
const { data: orgUsers } = useGetOrgUsers(orgId); const { data: orgUsers } = useGetOrgUsers(orgId);
const [searchMemberFilter, setSearchMemberFilter] = useState(""); const [searchMemberFilter, setSearchMemberFilter] = useState("");
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([ const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
@@ -99,7 +94,6 @@ export const MemberListTab = () => {
const { mutateAsync: addUserToWorkspace } = useAddUserToWsE2EE(); const { mutateAsync: addUserToWorkspace } = useAddUserToWsE2EE();
const { mutateAsync: addUserToWorkspaceNonE2EE } = useAddUserToWsNonE2EE(); const { mutateAsync: addUserToWorkspaceNonE2EE } = useAddUserToWsNonE2EE();
const { mutateAsync: uploadWsKey } = useUploadWsKey();
const { mutateAsync: removeUserFromWorkspace } = useDeleteUserFromWorkspace(); const { mutateAsync: removeUserFromWorkspace } = useDeleteUserFromWorkspace();
const { mutateAsync: updateUserWorkspaceRole } = useUpdateUserWorkspaceRole(); const { mutateAsync: updateUserWorkspaceRole } = useUpdateUserWorkspaceRole();
@@ -117,7 +111,7 @@ export const MemberListTab = () => {
const orgUser = (orgUsers || []).find(({ id }) => id === orgMembershipId); const orgUser = (orgUsers || []).find(({ id }) => id === orgMembershipId);
if (!orgUser) return; if (!orgUser) return;
try { try { // TODO: update
if (currentWorkspace.version === ProjectVersion.V1) { if (currentWorkspace.version === ProjectVersion.V1) {
await addUserToWorkspace({ await addUserToWorkspace({
workspaceId, workspaceId,
@@ -128,7 +122,7 @@ export const MemberListTab = () => {
} else if (currentWorkspace.version === ProjectVersion.V2) { } else if (currentWorkspace.version === ProjectVersion.V2) {
await addUserToWorkspaceNonE2EE({ await addUserToWorkspaceNonE2EE({
projectId: workspaceId, projectId: workspaceId,
emails: [orgUser.user.email] usernames: [orgUser.user.username]
}); });
} else { } else {
createNotification({ createNotification({
@@ -154,11 +148,11 @@ export const MemberListTab = () => {
}; };
const handleRemoveUser = async () => { const handleRemoveUser = async () => {
const email = (popUp?.removeMember?.data as { email: string })?.email; const username = (popUp?.removeMember?.data as { username: string })?.username;
if (!currentOrg?.id) return; if (!currentOrg?.id) return;
try { try {
await removeUserFromWorkspace({ workspaceId, emails: [email] }); await removeUserFromWorkspace({ workspaceId, usernames: [username] });
createNotification({ createNotification({
text: "Successfully removed user from project", text: "Successfully removed user from project",
type: "success" type: "success"
@@ -220,6 +214,7 @@ export const MemberListTab = () => {
({ user: u, inviteEmail }) => ({ user: u, inviteEmail }) =>
u?.firstName?.toLowerCase().includes(searchMemberFilter.toLowerCase()) || u?.firstName?.toLowerCase().includes(searchMemberFilter.toLowerCase()) ||
u?.lastName?.toLowerCase().includes(searchMemberFilter.toLowerCase()) || u?.lastName?.toLowerCase().includes(searchMemberFilter.toLowerCase()) ||
u?.username?.toLowerCase().includes(searchMemberFilter.toLowerCase()) ||
u?.email?.toLowerCase().includes(searchMemberFilter.toLowerCase()) || u?.email?.toLowerCase().includes(searchMemberFilter.toLowerCase()) ||
inviteEmail?.includes(searchMemberFilter.toLowerCase()) inviteEmail?.includes(searchMemberFilter.toLowerCase())
), ),
@@ -227,49 +222,15 @@ export const MemberListTab = () => {
); );
const filteredOrgUsers = useMemo(() => { const filteredOrgUsers = useMemo(() => {
const wsUserEmails = new Map(); const wsUserUsernames = new Map();
members?.forEach((member) => { members?.forEach((member) => {
wsUserEmails.set(member.user.email, true); wsUserUsernames.set(member.user.username, true);
}); });
return (orgUsers || []).filter( return (orgUsers || []).filter(
({ status, user: u }) => status === "accepted" && !wsUserEmails.has(u.email) ({ status, user: u }) => status === "accepted" && !wsUserUsernames.has(u.username)
); );
}, [orgUsers, members]); }, [orgUsers, members]);
const onGrantAccess = async (grantedUserId: string, publicKey: string) => {
try {
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
if (!PRIVATE_KEY || !wsKey) return;
// assymmetrically decrypt symmetric key with local private key
const key = decryptAssymmetric({
ciphertext: wsKey.encryptedKey,
nonce: wsKey.nonce,
publicKey: wsKey.sender.publicKey,
privateKey: PRIVATE_KEY
});
const { ciphertext, nonce } = encryptAssymmetric({
plaintext: key,
publicKey,
privateKey: PRIVATE_KEY
});
await uploadWsKey({
userId: grantedUserId,
nonce,
encryptedKey: ciphertext,
workspaceId: currentWorkspace?.id || ""
});
} catch (err) {
console.error(err);
createNotification({
text: "Failed to grant access to user",
type: "error"
});
}
};
const isLoading = isMembersLoading || isRolesLoading; const isLoading = isMembersLoading || isRolesLoading;
return ( return (
@@ -302,7 +263,7 @@ export const MemberListTab = () => {
<THead> <THead>
<Tr> <Tr>
<Th>Name</Th> <Th>Name</Th>
<Th>Email</Th> <Th>Username</Th>
<Th>Role</Th> <Th>Role</Th>
<Th className="w-5" /> <Th className="w-5" />
</Tr> </Tr>
@@ -311,22 +272,20 @@ export const MemberListTab = () => {
{isLoading && <TableSkeleton columns={4} innerKey="project-members" />} {isLoading && <TableSkeleton columns={4} innerKey="project-members" />}
{!isLoading && {!isLoading &&
filterdUsers?.map( filterdUsers?.map(
({ user: u, inviteEmail, id: membershipId, status, roleId, role }) => { ({ user: u, id: membershipId, roleId, role }) => {
const name = u ? `${u.firstName} ${u.lastName}` : "-"; const name = u ? `${u.firstName} ${u.lastName}` : "-";
const email = u?.email || inviteEmail; const username = u?.username ?? "-";
return ( return (
<Tr key={`membership-${membershipId}`} className="w-full"> <Tr key={`membership-${membershipId}`} className="w-full">
<Td>{name}</Td> <Td>{name}</Td>
<Td>{email}</Td> <Td>{username}</Td>
<Td> <Td>
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Edit} I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.Member} a={ProjectPermissionSub.Member}
> >
{(isAllowed) => ( {(isAllowed) => (
<> <Select
<Select
value={role === "custom" ? findRoleFromId(roleId)?.slug : role} value={role === "custom" ? findRoleFromId(roleId)?.slug : role}
isDisabled={userId === u?.id || !isAllowed} isDisabled={userId === u?.id || !isAllowed}
className="w-40 bg-mineshaft-600" className="w-40 bg-mineshaft-600"
@@ -345,18 +304,6 @@ export const MemberListTab = () => {
</SelectItem> </SelectItem>
))} ))}
</Select> </Select>
{status === "completed" && user.email !== email && (
<div className="rounded-md border border-mineshaft-700 bg-white/5 text-white duration-200 hover:bg-primary hover:text-black">
<Button
colorSchema="secondary"
isDisabled={!isAllowed}
onClick={() => onGrantAccess(u?.id, u?.publicKey)}
>
Grant Access
</Button>
</div>
)}
</>
)} )}
</ProjectPermissionCan> </ProjectPermissionCan>
</Td> </Td>
@@ -375,7 +322,7 @@ export const MemberListTab = () => {
className="ml-4" className="ml-4"
isDisabled={userId === u?.id || !isAllowed} isDisabled={userId === u?.id || !isAllowed}
onClick={() => onClick={() =>
handlePopUpOpen("removeMember", { email: u.email }) handlePopUpOpen("removeMember", { username: u.username })
} }
> >
<FontAwesomeIcon icon={faXmark} /> <FontAwesomeIcon icon={faXmark} />
@@ -407,20 +354,20 @@ export const MemberListTab = () => {
<form onSubmit={handleSubmit(onAddMember)}> <form onSubmit={handleSubmit(onAddMember)}>
<Controller <Controller
control={control} control={control}
defaultValue={filteredOrgUsers?.[0]?.user?.email} defaultValue={filteredOrgUsers?.[0]?.user?.username}
name="orgMembershipId" name="orgMembershipId"
render={({ field, fieldState: { error } }) => ( render={({ field, fieldState: { error } }) => (
<FormControl label="Email" isError={Boolean(error)} errorText={error?.message}> <FormControl label="Username" isError={Boolean(error)} errorText={error?.message}>
<Select <Select
position="popper" position="popper"
className="w-full" className="w-full"
defaultValue={filteredOrgUsers?.[0]?.user?.email} defaultValue={filteredOrgUsers?.[0]?.user?.username}
value={field.value} value={field.value}
onValueChange={field.onChange} onValueChange={field.onChange}
> >
{filteredOrgUsers.map(({ id: orgUserId, user: u }) => ( {filteredOrgUsers.map(({ id: orgUserId, user: u }) => (
<SelectItem value={orgUserId} key={`org-membership-join-${orgUserId}`}> <SelectItem value={orgUserId} key={`org-membership-join-${orgUserId}`}>
{u?.email} {u?.username}
</SelectItem> </SelectItem>
))} ))}
</Select> </Select>

Some files were not shown because too many files have changed in this diff Show More