diff --git a/backend/src/@types/knex.d.ts b/backend/src/@types/knex.d.ts index dadc2fac29..2e8b2611d9 100644 --- a/backend/src/@types/knex.d.ts +++ b/backend/src/@types/knex.d.ts @@ -164,6 +164,9 @@ import { TUserActions, TUserActionsInsert, TUserActionsUpdate, + TUserAliases, + TUserAliasesInsert, + TUserAliasesUpdate, TUserEncryptionKeys, TUserEncryptionKeysInsert, TUserEncryptionKeysUpdate, @@ -178,6 +181,7 @@ import { declare module "knex/types/tables" { interface Tables { [TableName.Users]: Knex.CompositeTableType; + [TableName.UserAliases]: Knex.CompositeTableType; [TableName.UserEncryptionKey]: Knex.CompositeTableType< TUserEncryptionKeys, TUserEncryptionKeysInsert, diff --git a/backend/src/db/migrations/20240305165532_ldap-config.ts b/backend/src/db/migrations/20240305165532_ldap-config.ts index a77dda8a7d..9d1415e792 100644 --- a/backend/src/db/migrations/20240305165532_ldap-config.ts +++ b/backend/src/db/migrations/20240305165532_ldap-config.ts @@ -25,24 +25,38 @@ export async function up(knex: Knex): Promise { }); } + 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").notNullable(); - t.uuid("orgId").nullable(); - t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE"); + t.string("username").unique().notNullable(); t.string("email").nullable().alter(); - t.unique(["username", "orgId"]); }); await knex(TableName.Users).update("username", knex.ref("email")); - - await createOnUpdateTrigger(knex, TableName.LdapConfig); } export async function down(knex: Knex): Promise { await knex.schema.dropTableIfExists(TableName.LdapConfig); + await knex.schema.dropTableIfExists(TableName.UserAliases); await knex.schema.alterTable(TableName.Users, (t) => { t.dropColumn("username"); - t.dropColumn("orgId"); // t.string("email").notNullable().alter(); }); await dropOnUpdateTrigger(knex, TableName.LdapConfig); diff --git a/backend/src/db/schemas/index.ts b/backend/src/db/schemas/index.ts index b70e978478..fb717d344c 100644 --- a/backend/src/db/schemas/index.ts +++ b/backend/src/db/schemas/index.ts @@ -53,6 +53,7 @@ export * from "./service-tokens"; export * from "./super-admin"; export * from "./trusted-ips"; export * from "./user-actions"; +export * from "./user-aliases"; export * from "./user-encryption-keys"; export * from "./users"; export * from "./webhooks"; diff --git a/backend/src/db/schemas/ldap-configs.ts b/backend/src/db/schemas/ldap-configs.ts index 653553bae0..cf0d96847c 100644 --- a/backend/src/db/schemas/ldap-configs.ts +++ b/backend/src/db/schemas/ldap-configs.ts @@ -27,5 +27,5 @@ export const LdapConfigsSchema = z.object({ }); export type TLdapConfigs = z.infer; -export type TLdapConfigsInsert = Omit; -export type TLdapConfigsUpdate = Partial>; +export type TLdapConfigsInsert = Omit, TImmutableDBKeys>; +export type TLdapConfigsUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts index d2c9bc4dc0..c85aad66ad 100644 --- a/backend/src/db/schemas/models.ts +++ b/backend/src/db/schemas/models.ts @@ -2,6 +2,7 @@ import { z } from "zod"; export enum TableName { Users = "users", + UserAliases = "user_aliases", UserEncryptionKey = "user_encryption_keys", AuthTokens = "auth_tokens", AuthTokenSession = "auth_token_sessions", diff --git a/backend/src/db/schemas/user-aliases.ts b/backend/src/db/schemas/user-aliases.ts new file mode 100644 index 0000000000..d8712fe751 --- /dev/null +++ b/backend/src/db/schemas/user-aliases.ts @@ -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; +export type TUserAliasesInsert = Omit, TImmutableDBKeys>; +export type TUserAliasesUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/users.ts b/backend/src/db/schemas/users.ts index c53e96b82a..86ee2fb74e 100644 --- a/backend/src/db/schemas/users.ts +++ b/backend/src/db/schemas/users.ts @@ -21,8 +21,7 @@ export const UsersSchema = z.object({ createdAt: z.date(), updatedAt: z.date(), isGhost: z.boolean().default(false), - username: z.string(), - orgId: z.string().uuid().nullable().optional() + username: z.string() }); export type TUsers = z.infer; diff --git a/backend/src/ee/routes/v1/ldap-router.ts b/backend/src/ee/routes/v1/ldap-router.ts index 4becd63716..ea33ab2cab 100644 --- a/backend/src/ee/routes/v1/ldap-router.ts +++ b/backend/src/ee/routes/v1/ldap-router.ts @@ -34,9 +34,11 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => { 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 }); diff --git a/backend/src/ee/routes/v1/scim-router.ts b/backend/src/ee/routes/v1/scim-router.ts index 8830c07772..2a3772cd6e 100644 --- a/backend/src/ee/routes/v1/scim-router.ts +++ b/backend/src/ee/routes/v1/scim-router.ts @@ -237,7 +237,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => { const user = await req.server.services.scim.createScimUser({ username: req.body.userName, - email: primaryEmail as string, + email: primaryEmail, firstName: req.body.name.givenName, lastName: req.body.name.familyName, orgId: req.permission.orgId as string diff --git a/backend/src/ee/services/ldap-config/ldap-config-service.ts b/backend/src/ee/services/ldap-config/ldap-config-service.ts index b78a2b1382..7e3623c1f0 100644 --- a/backend/src/ee/services/ldap-config/ldap-config-service.ts +++ b/backend/src/ee/services/ldap-config/ldap-config-service.ts @@ -19,6 +19,8 @@ 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"; @@ -34,6 +36,7 @@ type TLdapConfigServiceFactoryDep = { >; orgBotDAL: Pick; userDAL: Pick; + userAliasDAL: Pick; permissionService: Pick; licenseService: Pick; }; @@ -45,6 +48,7 @@ export const ldapConfigServiceFactory = ({ orgDAL, orgBotDAL, userDAL, + userAliasDAL, permissionService, licenseService }: TLdapConfigServiceFactoryDep) => { @@ -289,6 +293,8 @@ export const ldapConfigServiceFactory = ({ const boot = async () => { try { 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 @@ -302,7 +308,7 @@ export const ldapConfigServiceFactory = ({ bindCredentials: ldapConfig.bindPass, searchBase: ldapConfig.searchBase, searchFilter: "(uid={{username}})", - searchAttributes: ["uid", "givenName", "sn"], + searchAttributes: ["uid", "uidNumber", "givenName", "sn", "mail"], ...(ldapConfig.caCert !== "" ? { tlsOptions: { @@ -328,23 +334,25 @@ export const ldapConfigServiceFactory = ({ }); }; - const ldapLogin = async ({ username, firstName, lastName, orgId, relayState }: TLdapLoginDTO) => { + const ldapLogin = async ({ externalId, username, firstName, lastName, emails, orgId, relayState }: TLdapLoginDTO) => { + // externalId + username const appCfg = getConfig(); - let user = await userDAL.findOne({ - username, - orgId + 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 (user) { + if (userAlias) { await userDAL.transaction(async (tx) => { - const [orgMembership] = await orgDAL.findMembership({ userId: user.id }, { tx }); + const [orgMembership] = await orgDAL.findMembership({ userId: userAlias.userId }, { tx }); if (!orgMembership) { await orgDAL.createMembership( { - userId: user.id, + userId: userAlias.userId, orgId, role: OrgMembershipRole.Member, status: OrgMembershipStatus.Accepted @@ -362,11 +370,12 @@ export const ldapConfigServiceFactory = ({ } }); } else { - user = await userDAL.transaction(async (tx) => { + userAlias = await userDAL.transaction(async (tx) => { + const uniqueUsername = await normalizeUsername(username, userDAL); const newUser = await userDAL.create( { - username, - orgId, + username: uniqueUsername, + email: emails[0], firstName, lastName, authMethods: [AuthMethod.LDAP], @@ -374,6 +383,18 @@ export const ldapConfigServiceFactory = ({ }, tx ); + const newUserAlias = await userAliasDAL.create( + { + userId: newUser.id, + username, + aliasType: AuthMethod.LDAP, + externalId, + emails, + orgId + }, + tx + ); + await orgDAL.createMembership( { userId: newUser.id, @@ -384,10 +405,13 @@ export const ldapConfigServiceFactory = ({ tx ); - return newUser; + return newUserAlias; }); } + // query for user here + const user = await userDAL.findOne({ id: userAlias.userId }); + const isUserCompleted = Boolean(user.isAccepted); const providerAuthToken = jwt.sign( diff --git a/backend/src/ee/services/ldap-config/ldap-config-types.ts b/backend/src/ee/services/ldap-config/ldap-config-types.ts index 2630857359..025ce7781e 100644 --- a/backend/src/ee/services/ldap-config/ldap-config-types.ts +++ b/backend/src/ee/services/ldap-config/ldap-config-types.ts @@ -20,9 +20,11 @@ export type TUpdateLdapCfgDTO = Partial<{ TOrgPermission; export type TLdapLoginDTO = { + externalId: string; username: string; firstName: string; lastName: string; + emails: string[]; orgId: string; relayState?: string; }; diff --git a/backend/src/ee/services/license/__mocks__/licence-fns.ts b/backend/src/ee/services/license/__mocks__/licence-fns.ts index 8f52939c5b..3f00d7ad51 100644 --- a/backend/src/ee/services/license/__mocks__/licence-fns.ts +++ b/backend/src/ee/services/license/__mocks__/licence-fns.ts @@ -19,7 +19,7 @@ export const getDefaultOnPremFeatures = () => { auditLogsRetentionDays: 0, samlSSO: false, scim: false, - ldap: false, + ldap: true, status: null, trial_end: null, has_used_trial: true, diff --git a/backend/src/ee/services/license/licence-fns.ts b/backend/src/ee/services/license/licence-fns.ts index 8dca967370..fa23664f47 100644 --- a/backend/src/ee/services/license/licence-fns.ts +++ b/backend/src/ee/services/license/licence-fns.ts @@ -25,7 +25,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({ auditLogsRetentionDays: 0, samlSSO: false, scim: false, - ldap: false, + ldap: true, status: null, trial_end: null, has_used_trial: true, diff --git a/backend/src/ee/services/license/license-types.ts b/backend/src/ee/services/license/license-types.ts index 80f422380b..33ed2ba0ba 100644 --- a/backend/src/ee/services/license/license-types.ts +++ b/backend/src/ee/services/license/license-types.ts @@ -26,7 +26,7 @@ export type TFeatureSet = { auditLogsRetentionDays: 0; samlSSO: false; scim: false; - ldap: false; + ldap: true; status: null; trial_end: null; has_used_trial: true; diff --git a/backend/src/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-queue.ts b/backend/src/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-queue.ts index b776d3d906..1b19fd7f55 100644 --- a/backend/src/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-queue.ts +++ b/backend/src/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-queue.ts @@ -64,7 +64,7 @@ export const secretScanningQueueFactory = ({ orgId: organizationId, 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) => { @@ -149,7 +149,7 @@ export const secretScanningQueueFactory = ({ await smtpService.sendMail({ template: SmtpTemplates.SecretLeakIncident, subjectLine: `Incident alert: leaked secrets found in Github repository ${repository.fullName}`, - recipients: adminEmails.filter((email) => email).map((email) => email as string), + recipients: adminEmails.filter((email) => email).map((email) => email), substitutions: { numberOfSecrets: Object.keys(allFindingsByFingerprint).length, pusher_email: pusher.email, @@ -221,7 +221,7 @@ export const secretScanningQueueFactory = ({ await smtpService.sendMail({ template: SmtpTemplates.SecretLeakIncident, subjectLine: `Incident alert: leaked secrets found in Github repository ${repository.fullName}`, - recipients: adminEmails.filter((email) => email).map((email) => email as string), + recipients: adminEmails.filter((email) => email).map((email) => email), substitutions: { numberOfSecrets: findings.length } diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 45edf87e54..4e5f749a5a 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -104,6 +104,7 @@ import { telemetryQueueServiceFactory } from "@app/services/telemetry/telemetry- import { telemetryServiceFactory } from "@app/services/telemetry/telemetry-service"; import { userDALFactory } from "@app/services/user/user-dal"; 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 { webhookServiceFactory } from "@app/services/webhook/webhook-service"; @@ -128,6 +129,7 @@ export const registerRoutes = async ( // db layers const userDAL = userDALFactory(db); + const userAliasDAL = userAliasDALFactory(db); const authDAL = authDALFactory(db); const authTokenDAL = tokenDALFactory(db); const orgDAL = orgDALFactory(db); @@ -243,6 +245,7 @@ export const registerRoutes = async ( orgDAL, orgBotDAL, userDAL, + userAliasDAL, permissionService, licenseService }); diff --git a/backend/src/server/routes/v1/admin-router.ts b/backend/src/server/routes/v1/admin-router.ts index d8431a62f0..0ee0b0fe97 100644 --- a/backend/src/server/routes/v1/admin-router.ts +++ b/backend/src/server/routes/v1/admin-router.ts @@ -92,7 +92,7 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { await server.services.telemetry.sendPostHogEvents({ event: PostHogEventTypes.AdminInit, - distinctId: user.user.email ?? user.user.username ?? "", + distinctId: user.user.username ?? "", properties: { email: user.user.email ?? "", lastName: user.user.lastName || "", diff --git a/backend/src/server/routes/v2/project-membership-router.ts b/backend/src/server/routes/v2/project-membership-router.ts index 47a7e207c0..f63770346b 100644 --- a/backend/src/server/routes/v2/project-membership-router.ts +++ b/backend/src/server/routes/v2/project-membership-router.ts @@ -15,7 +15,7 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider }), body: z.object({ emails: z.string().email().array().default([]).describe("Emails of the users to add to the project."), - usernames: z.string().email().array().default([]).describe("Usernames of the users to add to the project.") + usernames: z.string().array().default([]).describe("Usernames of the users to add to the project.") }), response: { 200: z.object({ @@ -59,7 +59,8 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider }), 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: { 200: z.object({ @@ -74,7 +75,8 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider actor: req.permission.type, actorOrgId: req.permission.orgId, projectId: req.params.projectId, - emails: req.body.emails + emails: req.body.emails, + usernames: req.body.usernames }); for (const membership of memberships) { diff --git a/backend/src/server/routes/v3/login-router.ts b/backend/src/server/routes/v3/login-router.ts index 0c3f8b2238..240aa21b1e 100644 --- a/backend/src/server/routes/v3/login-router.ts +++ b/backend/src/server/routes/v3/login-router.ts @@ -13,7 +13,6 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => { schema: { body: z.object({ email: z.string().trim(), - orgId: z.string().optional(), providerAuthToken: z.string().trim().optional(), clientPublicKey: z.string().trim() }), @@ -27,7 +26,6 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => { handler: async (req) => { const { serverPublicKey, salt } = await server.services.login.loginGenServerPublicKey({ email: req.body.email, - userOrgId: req.body.orgId, clientPublicKey: req.body.clientPublicKey, providerAuthToken: req.body.providerAuthToken }); @@ -45,7 +43,6 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => { schema: { body: z.object({ email: z.string().trim(), - orgId: z.string().optional(), providerAuthToken: z.string().trim().optional(), clientProof: z.string().trim() }), @@ -74,7 +71,6 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => { const data = await server.services.login.loginExchangeClientProof({ email: req.body.email, - userOrgId: req.body.orgId, ip: req.realIp, userAgent, providerAuthToken: req.body.providerAuthToken, diff --git a/backend/src/server/routes/v3/signup-router.ts b/backend/src/server/routes/v3/signup-router.ts index 220eb62333..6488bcadcb 100644 --- a/backend/src/server/routes/v3/signup-router.ts +++ b/backend/src/server/routes/v3/signup-router.ts @@ -137,7 +137,7 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => { void server.services.telemetry.sendPostHogEvents({ event: PostHogEventTypes.UserSignedUp, - distinctId: user.email ?? user.username ?? "", + distinctId: user.username ?? "", properties: { email: user.email ?? "", attributionSource: req.body.attributionSource @@ -202,7 +202,7 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => { void server.services.telemetry.sendPostHogEvents({ event: PostHogEventTypes.UserSignedUp, - distinctId: user.email ?? user.username ?? "", + distinctId: user.username ?? "", properties: { email: user.email ?? "", attributionSource: "Team Invite" diff --git a/backend/src/services/auth/auth-login-service.ts b/backend/src/services/auth/auth-login-service.ts index ab490bdb43..0591a5e8de 100644 --- a/backend/src/services/auth/auth-login-service.ts +++ b/backend/src/services/auth/auth-login-service.ts @@ -130,13 +130,11 @@ export const authLoginServiceFactory = ({ userDAL, tokenService, smtpService }: */ const loginGenServerPublicKey = async ({ email, - userOrgId, providerAuthToken, clientPublicKey }: TLoginGenServerPublicKeyDTO) => { const userEnc = await userDAL.findUserEncKeyByUsername({ - username: email, - orgId: userOrgId + username: email }); if (!userEnc || (userEnc && !userEnc.isAccepted)) { throw new Error("Failed to find user"); @@ -159,15 +157,13 @@ export const authLoginServiceFactory = ({ userDAL, tokenService, smtpService }: */ const loginExchangeClientProof = async ({ email, - userOrgId, clientProof, providerAuthToken, ip, userAgent }: TLoginClientProofDTO) => { const userEnc = await userDAL.findUserEncKeyByUsername({ - username: email, - orgId: userOrgId + username: email }); if (!userEnc) throw new Error("Failed to find user"); const cfg = getConfig(); diff --git a/backend/src/services/auth/auth-login-type.ts b/backend/src/services/auth/auth-login-type.ts index 42a7dd968f..86af5a5f9e 100644 --- a/backend/src/services/auth/auth-login-type.ts +++ b/backend/src/services/auth/auth-login-type.ts @@ -2,14 +2,12 @@ import { AuthMethod } from "./auth-type"; export type TLoginGenServerPublicKeyDTO = { email: string; - userOrgId?: string; clientPublicKey: string; providerAuthToken?: string; }; export type TLoginClientProofDTO = { email: string; - userOrgId?: string; clientProof: string; providerAuthToken?: string; ip: string; diff --git a/backend/src/services/org/org-dal.ts b/backend/src/services/org/org-dal.ts index 794692c70d..3b1daa8277 100644 --- a/backend/src/services/org/org-dal.ts +++ b/backend/src/services/org/org-dal.ts @@ -92,7 +92,7 @@ export const orgDALFactory = (db: TDbClient) => { const findOrgMembersByUsername = async (orgId: string, usernames: string[]) => { try { const members = await db(TableName.OrgMembership) - .where({ orgId }) + .where(`${TableName.OrgMembership}.orgId`, orgId) .join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`) .leftJoin( TableName.UserEncryptionKey, diff --git a/backend/src/services/org/org-service.ts b/backend/src/services/org/org-service.ts index c12da6cf35..296c9936c1 100644 --- a/backend/src/services/org/org-service.ts +++ b/backend/src/services/org/org-service.ts @@ -441,7 +441,7 @@ export const orgServiceFactory = ({ recipients: [inviteeEmail], substitutions: { inviterFirstName: user.firstName, - inviterEmail: user.email, + inviterUsername: user.username, organizationName: org?.name, email: inviteeEmail, organizationId: org?.id.toString(), diff --git a/backend/src/services/project-membership/project-membership-dal.ts b/backend/src/services/project-membership/project-membership-dal.ts index ea2902a739..02bf28d011 100644 --- a/backend/src/services/project-membership/project-membership-dal.ts +++ b/backend/src/services/project-membership/project-membership-dal.ts @@ -57,7 +57,7 @@ export const projectMembershipDALFactory = (db: TDbClient) => { } }; - const findMembershipsByEmail = async (projectId: string, emails: string[]) => { + const findMembershipsByUsername = async (projectId: string, usernames: string[]) => { try { const members = await db(TableName.ProjectMembership) .where({ projectId }) @@ -70,18 +70,18 @@ export const projectMembershipDALFactory = (db: TDbClient) => { .select( selectAllTableCols(TableName.ProjectMembership), 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 }); - return members.map(({ userId, email, ...data }) => ({ + return members.map(({ userId, username, ...data }) => ({ ...data, - user: { id: userId, email } + user: { id: userId, username } })); } catch (error) { throw new DatabaseError({ error, name: "Find members by email" }); } }; - return { ...projectMemberOrm, findAllProjectMembers, findProjectGhostUser, findMembershipsByEmail }; + return { ...projectMemberOrm, findAllProjectMembers, findProjectGhostUser, findMembershipsByUsername }; }; diff --git a/backend/src/services/project-membership/project-membership-service.ts b/backend/src/services/project-membership/project-membership-service.ts index 4b569976da..70d1c80237 100644 --- a/backend/src/services/project-membership/project-membership-service.ts +++ b/backend/src/services/project-membership/project-membership-service.ts @@ -134,7 +134,7 @@ export const projectMembershipServiceFactory = ({ const appCfg = getConfig(); await smtpService.sendMail({ template: SmtpTemplates.WorkspaceInvite, - subjectLine: "Infisical workspace invitation", + subjectLine: "Infisical project invitation", recipients: invitees.filter((i) => i.email).map((i) => i.email as string), substitutions: { workspaceName: project.name, @@ -206,10 +206,8 @@ export const projectMembershipServiceFactory = ({ const appCfg = getConfig(); await smtpService.sendMail({ template: SmtpTemplates.WorkspaceInvite, - subjectLine: "Infisical workspace invitation", - recipients: orgMembers - .map(({ email }) => email) - .filter((email): email is string => email !== null && email !== undefined), + subjectLine: "Infisical project invitation", + recipients: orgMembers.filter((i) => i.email).map((i) => i.email as string), substitutions: { workspaceName: project.name, callback_url: `${appCfg.SITE_URL}/login` @@ -237,11 +235,14 @@ export const projectMembershipServiceFactory = ({ const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Member); + const usernamesAndEmails = [...emails, ...usernames]; + const orgMembers = await orgDAL.findOrgMembersByUsername(project.orgId, [ - ...new Set([...emails, ...usernames].map((element) => element.toLowerCase())) + ...new Set(usernamesAndEmails.map((element) => element.toLowerCase())) ]); - if (orgMembers.length !== emails.length) throw new BadRequestError({ message: "Some users are not part of org" }); + if (orgMembers.length !== usernamesAndEmails.length) + throw new BadRequestError({ message: "Some users are not part of org" }); if (!orgMembers.length) return []; @@ -320,16 +321,21 @@ export const projectMembershipServiceFactory = ({ }); if (sendEmails) { + const recipients = orgMembers.filter((i) => i.user.email).map((i) => i.user.email as string); + const appCfg = getConfig(); - await smtpService.sendMail({ - template: SmtpTemplates.WorkspaceInvite, - subjectLine: "Infisical workspace invitation", - recipients: orgMembers.filter(({ user }) => user.email).map(({ user }) => user.email as string), - substitutions: { - workspaceName: project.name, - callback_url: `${appCfg.SITE_URL}/login` - } - }); + + if (recipients.length) { + await smtpService.sendMail({ + template: SmtpTemplates.WorkspaceInvite, + subjectLine: "Infisical project invitation", + recipients: orgMembers.filter((i) => i.user.email).map((i) => i.user.email as string), + substitutions: { + workspaceName: project.name, + callback_url: `${appCfg.SITE_URL}/login` + } + }); + } } return members; }; @@ -412,7 +418,8 @@ export const projectMembershipServiceFactory = ({ actor, actorOrgId, projectId, - emails + emails, + usernames }: TDeleteProjectMembershipsDTO) => { const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Member); @@ -426,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({ message: "Some users are not part of project", name: "Delete project membership" diff --git a/backend/src/services/project-membership/project-membership-types.ts b/backend/src/services/project-membership/project-membership-types.ts index 3f42f59851..abe4d2f72c 100644 --- a/backend/src/services/project-membership/project-membership-types.ts +++ b/backend/src/services/project-membership/project-membership-types.ts @@ -18,6 +18,7 @@ export type TDeleteProjectMembershipOldDTO = { export type TDeleteProjectMembershipsDTO = { emails: string[]; + usernames: string[]; } & TProjectPermission; export type TAddUsersToWorkspaceDTO = { diff --git a/backend/src/services/smtp/templates/organizationInvitation.handlebars b/backend/src/services/smtp/templates/organizationInvitation.handlebars index b281786f4e..024fca1321 100644 --- a/backend/src/services/smtp/templates/organizationInvitation.handlebars +++ b/backend/src/services/smtp/templates/organizationInvitation.handlebars @@ -8,7 +8,7 @@

Join your organization on Infisical

-

{{inviterFirstName}} ({{inviterEmail}}) has invited you to their Infisical organization — {{organizationName}}

+

{{inviterFirstName}} ({{inviterUsername}}) has invited you to their Infisical organization — {{organizationName}}

Join now

What is Infisical?

Infisical is an easy-to-use end-to-end encrypted tool that enables developers to sync and manage their secrets and configs.

diff --git a/backend/src/services/user-alias/user-alias-dal.ts b/backend/src/services/user-alias/user-alias-dal.ts new file mode 100644 index 0000000000..366eadce0e --- /dev/null +++ b/backend/src/services/user-alias/user-alias-dal.ts @@ -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; + +export const userAliasDALFactory = (db: TDbClient) => { + const userAliasOrm = ormify(db, TableName.UserAliases); + + return { + ...userAliasOrm + }; +}; diff --git a/backend/src/services/user-alias/user-alias-types.ts b/backend/src/services/user-alias/user-alias-types.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/backend/src/services/user/user-dal.ts b/backend/src/services/user/user-dal.ts index 7488da560e..0ef3f8dae5 100644 --- a/backend/src/services/user/user-dal.ts +++ b/backend/src/services/user/user-dal.ts @@ -20,12 +20,11 @@ export const userDALFactory = (db: TDbClient) => { // USER ENCRYPTION FUNCTIONS // ------------------------- - const findUserEncKeyByUsername = async ({ username, orgId }: { username: string; orgId?: string }) => { + const findUserEncKeyByUsername = async ({ username }: { username: string }) => { try { return await db(TableName.Users) .where({ username, - ...(orgId ? { orgId } : { orgId: null }), isGhost: false }) .join(TableName.UserEncryptionKey, `${TableName.Users}.id`, `${TableName.UserEncryptionKey}.userId`) diff --git a/backend/src/services/user/user-fns.ts b/backend/src/services/user/user-fns.ts new file mode 100644 index 0000000000..23789df1bd --- /dev/null +++ b/backend/src/services/user/user-fns.ts @@ -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) => { + 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; + } + } +}; diff --git a/frontend/src/components/utilities/attemptLogin.ts b/frontend/src/components/utilities/attemptLogin.ts index 4942a6ae6c..195cf9b9a2 100644 --- a/frontend/src/components/utilities/attemptLogin.ts +++ b/frontend/src/components/utilities/attemptLogin.ts @@ -21,12 +21,10 @@ interface IsLoginSuccessful { */ const attemptLogin = async ({ email, - orgId, password, providerAuthToken }: { email: string; - orgId?: string; password: string; providerAuthToken?: string; }): Promise => { @@ -40,7 +38,6 @@ const attemptLogin = async ({ const { serverPublicKey, salt } = await login1({ email, - orgId, clientPublicKey, providerAuthToken }); @@ -62,7 +59,6 @@ const attemptLogin = async ({ tag } = await login2({ email, - orgId, clientProof, providerAuthToken }); diff --git a/frontend/src/hooks/api/auth/types.ts b/frontend/src/hooks/api/auth/types.ts index c4e4e6fcea..a18d023c46 100644 --- a/frontend/src/hooks/api/auth/types.ts +++ b/frontend/src/hooks/api/auth/types.ts @@ -25,14 +25,12 @@ export type VerifyMfaTokenRes = { export type Login1DTO = { email: string; - orgId?: string; clientPublicKey: string; providerAuthToken?: string; } export type Login2DTO = { email: string; - orgId?: string; clientProof: string; providerAuthToken?: string; } diff --git a/frontend/src/hooks/api/users/mutation.tsx b/frontend/src/hooks/api/users/mutation.tsx index 7d982cea1b..a5c77b15fb 100644 --- a/frontend/src/hooks/api/users/mutation.tsx +++ b/frontend/src/hooks/api/users/mutation.tsx @@ -50,9 +50,9 @@ export const useAddUserToWsNonE2EE = () => { const queryClient = useQueryClient(); return useMutation<{}, {}, AddUserToWsDTONonE2EE>({ - mutationFn: async ({ projectId, emails }) => { + mutationFn: async ({ projectId, usernames }) => { const { data } = await apiRequest.post(`/api/v2/workspace/${projectId}/memberships`, { - emails + usernames }); return data; }, diff --git a/frontend/src/hooks/api/users/types.ts b/frontend/src/hooks/api/users/types.ts index 266e8aed29..65a6036986 100644 --- a/frontend/src/hooks/api/users/types.ts +++ b/frontend/src/hooks/api/users/types.ts @@ -78,7 +78,7 @@ export type AddUserToWsDTOE2EE = { export type AddUserToWsDTONonE2EE = { projectId: string; - emails: string[]; + usernames: string[]; }; export type UpdateOrgUserRoleDTO = { diff --git a/frontend/src/hooks/api/workspace/queries.tsx b/frontend/src/hooks/api/workspace/queries.tsx index d970cab54f..2d28be501b 100644 --- a/frontend/src/hooks/api/workspace/queries.tsx +++ b/frontend/src/hooks/api/workspace/queries.tsx @@ -323,11 +323,11 @@ export const useDeleteUserFromWorkspace = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async ({ emails, workspaceId }: { workspaceId: string; emails: string[] }) => { + mutationFn: async ({ usernames, workspaceId }: { workspaceId: string; usernames: string[] }) => { const { data: { deletedMembership } } = await apiRequest.delete(`/api/v2/workspace/${workspaceId}/memberships`, { - data: { emails } + data: { usernames } }); return deletedMembership; }, diff --git a/frontend/src/views/Login/components/PasswordStep/PasswordStep.tsx b/frontend/src/views/Login/components/PasswordStep/PasswordStep.tsx index 50355362ce..6924da3e75 100644 --- a/frontend/src/views/Login/components/PasswordStep/PasswordStep.tsx +++ b/frontend/src/views/Login/components/PasswordStep/PasswordStep.tsx @@ -78,7 +78,6 @@ export const PasswordStep = ({ } else { const loginAttempt = await attemptLogin({ email, - orgId: organizationId, password, providerAuthToken, }); diff --git a/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberListTab.tsx b/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberListTab.tsx index 8a06485e48..bae397f67f 100644 --- a/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberListTab.tsx +++ b/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberListTab.tsx @@ -111,7 +111,7 @@ export const MemberListTab = () => { const orgUser = (orgUsers || []).find(({ id }) => id === orgMembershipId); if (!orgUser) return; - try { + try { // TODO: update if (currentWorkspace.version === ProjectVersion.V1) { await addUserToWorkspace({ workspaceId, @@ -122,7 +122,7 @@ export const MemberListTab = () => { } else if (currentWorkspace.version === ProjectVersion.V2) { await addUserToWorkspaceNonE2EE({ projectId: workspaceId, - emails: [orgUser.user.username] + usernames: [orgUser.user.username] }); } else { createNotification({ @@ -148,11 +148,11 @@ export const MemberListTab = () => { }; 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; try { - await removeUserFromWorkspace({ workspaceId, emails: [email] }); + await removeUserFromWorkspace({ workspaceId, usernames: [username] }); createNotification({ text: "Successfully removed user from project", type: "success" @@ -222,12 +222,12 @@ export const MemberListTab = () => { ); const filteredOrgUsers = useMemo(() => { - const wsUserEmails = new Map(); + const wsUserUsernames = new Map(); members?.forEach((member) => { - wsUserEmails.set(member.user.email, true); + wsUserUsernames.set(member.user.username, true); }); return (orgUsers || []).filter( - ({ status, user: u }) => status === "accepted" && !wsUserEmails.has(u.email) + ({ status, user: u }) => status === "accepted" && !wsUserUsernames.has(u.username) ); }, [orgUsers, members]); @@ -322,7 +322,7 @@ export const MemberListTab = () => { className="ml-4" isDisabled={userId === u?.id || !isAllowed} onClick={() => - handlePopUpOpen("removeMember", { email: u.email }) + handlePopUpOpen("removeMember", { username: u.username }) } > @@ -354,20 +354,20 @@ export const MemberListTab = () => {
( - +