feat: implemented backend updates for email casing issue

This commit is contained in:
=
2025-05-15 00:56:52 +05:30
parent 6188de43e4
commit f85add7cca
19 changed files with 304 additions and 89 deletions
@@ -0,0 +1,47 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasEmail = await knex.schema.hasColumn(TableName.Users, "email");
const hasUsername = await knex.schema.hasColumn(TableName.Users, "username");
if (hasEmail) {
await knex(TableName.Users)
.where({ isGhost: false })
.update({
// @ts-expect-error email assume string this is expected
email: knex.raw("lower(email)")
});
}
if (hasUsername) {
await knex.schema.raw(`
CREATE INDEX IF NOT EXISTS ${TableName.Users}_lower_username_idx
ON ${TableName.Users} (LOWER(username))
`);
const duplicatesSubquery = knex(TableName.Users)
.select(knex.raw("lower(username) as lowercase_username"))
.groupBy("lowercase_username")
.having(knex.raw("count(*)"), ">", 1);
// Update usernames to lowercase where they won't create duplicates
await knex(TableName.Users)
.where({ isGhost: false })
.whereRaw("username <> lower(username)") // Only update if not already lowercase
// @ts-expect-error username assume string this is expected
.whereNotIn(knex.raw("lower(username)"), duplicatesSubquery)
.update({
// @ts-expect-error username assume string this is expected
username: knex.raw("lower(username)")
});
}
}
export async function down(knex: Knex): Promise<void> {
const hasUsername = await knex.schema.hasColumn(TableName.Users, "username");
if (hasUsername) {
await knex.schema.raw(`
DROP INDEX IF EXISTS ${TableName.Users}_lower_username_idx
`);
}
}
+2 -2
View File
@@ -111,9 +111,9 @@ export const groupDALFactory = (db: TDbClient) => {
} }
if (search) { if (search) {
void query.andWhereRaw(`CONCAT_WS(' ', "firstName", "lastName", "username") ilike ?`, [`%${search}%`]); void query.andWhereRaw(`CONCAT_WS(' ', "firstName", "lastName", lower("username")) ilike ?`, [`%${search}%`]);
} else if (username) { } else if (username) {
void query.andWhere(`${TableName.Users}.username`, "ilike", `%${username}%`); void query.andWhereRaw(`lower("${TableName.Users}"."username") ilike ?`, `%${username}%`);
} }
switch (filter) { switch (filter) {
@@ -30,7 +30,7 @@ import {
import { TUserGroupMembershipDALFactory } from "./user-group-membership-dal"; import { TUserGroupMembershipDALFactory } from "./user-group-membership-dal";
type TGroupServiceFactoryDep = { type TGroupServiceFactoryDep = {
userDAL: Pick<TUserDALFactory, "find" | "findUserEncKeyByUserIdsBatch" | "transaction" | "findOne">; userDAL: Pick<TUserDALFactory, "find" | "findUserEncKeyByUserIdsBatch" | "transaction" | "findUserByUsername">;
groupDAL: Pick< groupDAL: Pick<
TGroupDALFactory, TGroupDALFactory,
"create" | "findOne" | "update" | "delete" | "findAllGroupPossibleMembers" | "findById" | "transaction" "create" | "findOne" | "update" | "delete" | "findAllGroupPossibleMembers" | "findById" | "transaction"
@@ -380,7 +380,10 @@ export const groupServiceFactory = ({
details: { missingPermissions: permissionBoundary.missingPermissions } details: { missingPermissions: permissionBoundary.missingPermissions }
}); });
const user = await userDAL.findOne({ username }); const usersWithUsername = await userDAL.findUserByUsername(username);
// akhilmhdh: case sensitive email resolution
const user =
usersWithUsername?.length > 1 ? usersWithUsername.find((el) => el.username === username) : usersWithUsername?.[0];
if (!user) throw new NotFoundError({ message: `Failed to find user with username ${username}` }); if (!user) throw new NotFoundError({ message: `Failed to find user with username ${username}` });
const users = await addUsersToGroupByUserIds({ const users = await addUsersToGroupByUserIds({
@@ -461,7 +464,10 @@ export const groupServiceFactory = ({
details: { missingPermissions: permissionBoundary.missingPermissions } details: { missingPermissions: permissionBoundary.missingPermissions }
}); });
const user = await userDAL.findOne({ username }); const usersWithUsername = await userDAL.findUserByUsername(username);
// akhilmhdh: case sensitive email resolution
const user =
usersWithUsername?.length > 1 ? usersWithUsername.find((el) => el.username === username) : usersWithUsername?.[0];
if (!user) throw new NotFoundError({ message: `Failed to find user with username ${username}` }); if (!user) throw new NotFoundError({ message: `Failed to find user with username ${username}` });
const users = await removeUsersFromGroupByUserIds({ const users = await removeUsersFromGroupByUserIds({
@@ -380,7 +380,7 @@ export const ldapConfigServiceFactory = ({
if (serverCfg.trustLdapEmails) { if (serverCfg.trustLdapEmails) {
newUser = await userDAL.findOne( newUser = await userDAL.findOne(
{ {
email, email: email.toLowerCase(),
isEmailVerified: true isEmailVerified: true
}, },
tx tx
@@ -391,8 +391,8 @@ export const ldapConfigServiceFactory = ({
const uniqueUsername = await normalizeUsername(username, userDAL); const uniqueUsername = await normalizeUsername(username, userDAL);
newUser = await userDAL.create( newUser = await userDAL.create(
{ {
username: serverCfg.trustLdapEmails ? email : uniqueUsername, username: serverCfg.trustLdapEmails ? email.toLowerCase() : uniqueUsername,
email, email: email.toLowerCase(),
isEmailVerified: serverCfg.trustLdapEmails, isEmailVerified: serverCfg.trustLdapEmails,
firstName, firstName,
lastName, lastName,
@@ -429,7 +429,7 @@ export const ldapConfigServiceFactory = ({
await orgMembershipDAL.create( await orgMembershipDAL.create(
{ {
userId: newUser.id, userId: newUser.id,
inviteEmail: email, inviteEmail: email.toLowerCase(),
orgId, orgId,
role, role,
roleId, roleId,
@@ -30,7 +30,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
auditLogsRetentionDays: 0, auditLogsRetentionDays: 0,
auditLogStreams: false, auditLogStreams: false,
auditLogStreamLimit: 3, auditLogStreamLimit: 3,
samlSSO: false, samlSSO: true,
hsm: false, hsm: false,
oidcSSO: false, oidcSSO: false,
scim: false, scim: false,
@@ -171,8 +171,8 @@ export const oidcConfigServiceFactory = ({
}; };
const oidcLogin = async ({ const oidcLogin = async ({
externalId,
email, email,
externalId,
firstName, firstName,
lastName, lastName,
orgId, orgId,
@@ -181,6 +181,7 @@ export const oidcConfigServiceFactory = ({
manageGroupMemberships manageGroupMemberships
}: TOidcLoginDTO) => { }: TOidcLoginDTO) => {
const serverCfg = await getServerCfg(); const serverCfg = await getServerCfg();
const sanitizedEmail = email.toLowerCase();
if (serverCfg.enabledLoginMethods && !serverCfg.enabledLoginMethods.includes(LoginMethod.OIDC)) { if (serverCfg.enabledLoginMethods && !serverCfg.enabledLoginMethods.includes(LoginMethod.OIDC)) {
throw new ForbiddenRequestError({ throw new ForbiddenRequestError({
@@ -215,7 +216,7 @@ export const oidcConfigServiceFactory = ({
await orgMembershipDAL.create( await orgMembershipDAL.create(
{ {
userId: userAlias.userId, userId: userAlias.userId,
inviteEmail: email, inviteEmail: sanitizedEmail,
orgId, orgId,
role, role,
roleId, roleId,
@@ -245,7 +246,7 @@ export const oidcConfigServiceFactory = ({
// we prioritize getting the most complete user to create the new alias under // we prioritize getting the most complete user to create the new alias under
newUser = await userDAL.findOne( newUser = await userDAL.findOne(
{ {
email, email: sanitizedEmail,
isEmailVerified: true isEmailVerified: true
}, },
tx tx
@@ -255,7 +256,7 @@ export const oidcConfigServiceFactory = ({
// this fetches user entries created via invites // this fetches user entries created via invites
newUser = await userDAL.findOne( newUser = await userDAL.findOne(
{ {
username: email username: sanitizedEmail
}, },
tx tx
); );
@@ -273,10 +274,10 @@ export const oidcConfigServiceFactory = ({
const uniqueUsername = await normalizeUsername(externalId, userDAL); const uniqueUsername = await normalizeUsername(externalId, userDAL);
newUser = await userDAL.create( newUser = await userDAL.create(
{ {
email, email: sanitizedEmail,
firstName, firstName,
isEmailVerified: serverCfg.trustOidcEmails, isEmailVerified: serverCfg.trustOidcEmails,
username: serverCfg.trustOidcEmails ? email : uniqueUsername, username: serverCfg.trustOidcEmails ? sanitizedEmail : uniqueUsername,
lastName, lastName,
authMethods: [], authMethods: [],
isGhost: false isGhost: false
@@ -290,7 +291,7 @@ export const oidcConfigServiceFactory = ({
userId: newUser.id, userId: newUser.id,
aliasType: UserAliasType.OIDC, aliasType: UserAliasType.OIDC,
externalId, externalId,
emails: email ? [email] : [], emails: sanitizedEmail ? [sanitizedEmail] : [],
orgId orgId
}, },
tx tx
@@ -310,7 +311,7 @@ export const oidcConfigServiceFactory = ({
await orgMembershipDAL.create( await orgMembershipDAL.create(
{ {
userId: newUser.id, userId: newUser.id,
inviteEmail: email, inviteEmail: sanitizedEmail,
orgId, orgId,
role, role,
roleId, roleId,
@@ -266,7 +266,7 @@ export const samlConfigServiceFactory = ({
await orgMembershipDAL.create( await orgMembershipDAL.create(
{ {
userId: userAlias.userId, userId: userAlias.userId,
inviteEmail: email, inviteEmail: email.toLowerCase(),
orgId, orgId,
role, role,
roleId, roleId,
@@ -324,7 +324,7 @@ export const samlConfigServiceFactory = ({
if (serverCfg.trustSamlEmails) { if (serverCfg.trustSamlEmails) {
newUser = await userDAL.findOne( newUser = await userDAL.findOne(
{ {
email, email: email.toLowerCase(),
isEmailVerified: true isEmailVerified: true
}, },
tx tx
@@ -335,8 +335,8 @@ export const samlConfigServiceFactory = ({
const uniqueUsername = await normalizeUsername(`${firstName ?? ""}-${lastName ?? ""}`, userDAL); const uniqueUsername = await normalizeUsername(`${firstName ?? ""}-${lastName ?? ""}`, userDAL);
newUser = await userDAL.create( newUser = await userDAL.create(
{ {
username: serverCfg.trustSamlEmails ? email : uniqueUsername, username: serverCfg.trustSamlEmails ? email.toLowerCase() : uniqueUsername,
email, email: email.toLowerCase(),
isEmailVerified: serverCfg.trustSamlEmails, isEmailVerified: serverCfg.trustSamlEmails,
firstName, firstName,
lastName, lastName,
@@ -352,7 +352,7 @@ export const samlConfigServiceFactory = ({
userId: newUser.id, userId: newUser.id,
aliasType: UserAliasType.SAML, aliasType: UserAliasType.SAML,
externalId, externalId,
emails: email ? [email] : [], emails: email ? [email.toLowerCase()] : [],
orgId orgId
}, },
tx tx
@@ -372,7 +372,7 @@ export const samlConfigServiceFactory = ({
await orgMembershipDAL.create( await orgMembershipDAL.create(
{ {
userId: newUser.id, userId: newUser.id,
inviteEmail: email, inviteEmail: email.toLowerCase(),
orgId, orgId,
role, role,
roleId, roleId,
+8 -8
View File
@@ -342,7 +342,7 @@ export const scimServiceFactory = ({
orgMembership = await orgMembershipDAL.create( orgMembership = await orgMembershipDAL.create(
{ {
userId: userAlias.userId, userId: userAlias.userId,
inviteEmail: email, inviteEmail: email.toLowerCase(),
orgId, orgId,
role, role,
roleId, roleId,
@@ -364,7 +364,7 @@ export const scimServiceFactory = ({
if (trustScimEmails) { if (trustScimEmails) {
user = await userDAL.findOne( user = await userDAL.findOne(
{ {
email, email: email.toLowerCase(),
isEmailVerified: true isEmailVerified: true
}, },
tx tx
@@ -379,8 +379,8 @@ export const scimServiceFactory = ({
); );
user = await userDAL.create( user = await userDAL.create(
{ {
username: trustScimEmails ? email : uniqueUsername, username: trustScimEmails ? email.toLowerCase() : uniqueUsername,
email, email: email.toLowerCase(),
isEmailVerified: trustScimEmails, isEmailVerified: trustScimEmails,
firstName, firstName,
lastName, lastName,
@@ -396,7 +396,7 @@ export const scimServiceFactory = ({
userId: user.id, userId: user.id,
aliasType, aliasType,
externalId, externalId,
emails: email ? [email] : [], emails: email ? [email.toLowerCase()] : [],
orgId orgId
}, },
tx tx
@@ -418,7 +418,7 @@ export const scimServiceFactory = ({
orgMembership = await orgMembershipDAL.create( orgMembership = await orgMembershipDAL.create(
{ {
userId: user.id, userId: user.id,
inviteEmail: email, inviteEmail: email.toLowerCase(),
orgId, orgId,
role, role,
roleId, roleId,
@@ -529,7 +529,7 @@ export const scimServiceFactory = ({
membership.userId, membership.userId,
{ {
firstName: scimUser.name.givenName, firstName: scimUser.name.givenName,
email: scimUser.emails[0].value, email: scimUser.emails[0].value.toLowerCase(),
lastName: scimUser.name.familyName, lastName: scimUser.name.familyName,
isEmailVerified: hasEmailChanged ? trustScimEmails : undefined isEmailVerified: hasEmailChanged ? trustScimEmails : undefined
}, },
@@ -606,7 +606,7 @@ export const scimServiceFactory = ({
membership.userId, membership.userId,
{ {
firstName, firstName,
email, email: email?.toLowerCase(),
lastName, lastName,
isEmailVerified: isEmailVerified:
org.orgAuthMethod === OrgAuthMethod.OIDC ? serverCfg.trustOidcEmails : serverCfg.trustSamlEmails org.orgAuthMethod === OrgAuthMethod.OIDC ? serverCfg.trustOidcEmails : serverCfg.trustSamlEmails
+22 -4
View File
@@ -1,6 +1,8 @@
import { Knex } from "knex"; import { Knex } from "knex";
import { Compare, Filter, parse } from "scim2-parse-filter"; import { Compare, Filter, parse } from "scim2-parse-filter";
import { TableName } from "@app/db/schemas";
const appendParentToGroupingOperator = (parentPath: string, filter: Filter) => { const appendParentToGroupingOperator = (parentPath: string, filter: Filter) => {
if (filter.op !== "[]" && filter.op !== "and" && filter.op !== "or" && filter.op !== "not") { if (filter.op !== "[]" && filter.op !== "and" && filter.op !== "or" && filter.op !== "not") {
return { ...filter, attrPath: `${parentPath}.${(filter as Compare).attrPath}` }; return { ...filter, attrPath: `${parentPath}.${(filter as Compare).attrPath}` };
@@ -27,8 +29,12 @@ const processDynamicQuery = (
const { scimFilterAst, query } = stack.pop()!; const { scimFilterAst, query } = stack.pop()!;
switch (scimFilterAst.op) { switch (scimFilterAst.op) {
case "eq": { case "eq": {
let sanitizedValue = scimFilterAst.compValue;
const attrPath = getAttributeField(scimFilterAst.attrPath); const attrPath = getAttributeField(scimFilterAst.attrPath);
if (attrPath) void query.where(attrPath, scimFilterAst.compValue); if (attrPath === `${TableName.Users}.email` && typeof sanitizedValue === "string") {
sanitizedValue = sanitizedValue.toLowerCase();
}
if (attrPath) void query.where(attrPath, sanitizedValue);
break; break;
} }
case "pr": { case "pr": {
@@ -62,18 +68,30 @@ const processDynamicQuery = (
break; break;
} }
case "ew": { case "ew": {
let sanitizedValue = scimFilterAst.compValue;
const attrPath = getAttributeField(scimFilterAst.attrPath); const attrPath = getAttributeField(scimFilterAst.attrPath);
if (attrPath) void query.whereILike(attrPath, `%${scimFilterAst.compValue}`); if (attrPath === `${TableName.Users}.email` && typeof sanitizedValue === "string") {
sanitizedValue = sanitizedValue.toLowerCase();
}
if (attrPath) void query.whereILike(attrPath, `%${sanitizedValue}`);
break; break;
} }
case "co": { case "co": {
let sanitizedValue = scimFilterAst.compValue;
const attrPath = getAttributeField(scimFilterAst.attrPath); const attrPath = getAttributeField(scimFilterAst.attrPath);
if (attrPath) void query.whereILike(attrPath, `%${scimFilterAst.compValue}%`); if (attrPath === `${TableName.Users}.email` && typeof sanitizedValue === "string") {
sanitizedValue = sanitizedValue.toLowerCase();
}
if (attrPath) void query.whereILike(attrPath, `%${sanitizedValue}%`);
break; break;
} }
case "ne": { case "ne": {
let sanitizedValue = scimFilterAst.compValue;
const attrPath = getAttributeField(scimFilterAst.attrPath); const attrPath = getAttributeField(scimFilterAst.attrPath);
if (attrPath) void query.whereNot(attrPath, "=", scimFilterAst.compValue); if (attrPath === `${TableName.Users}.email` && typeof sanitizedValue === "string") {
sanitizedValue = sanitizedValue.toLowerCase();
}
if (attrPath) void query.whereNot(attrPath, "=", sanitizedValue);
break; break;
} }
case "and": { case "and": {
@@ -16,7 +16,12 @@ export const registerInviteOrgRouter = async (server: FastifyZodProvider) => {
method: "POST", method: "POST",
schema: { schema: {
body: z.object({ body: z.object({
inviteeEmails: z.array(z.string().trim().email()), inviteeEmails: z
.string()
.trim()
.email()
.array()
.refine((val) => val.every((el) => el === el.toLowerCase()), "Email must be lowercase"),
organizationId: z.string().trim(), organizationId: z.string().trim(),
projects: z projects: z
.object({ .object({
@@ -115,7 +120,11 @@ export const registerInviteOrgRouter = async (server: FastifyZodProvider) => {
}, },
schema: { schema: {
body: z.object({ body: z.object({
email: z.string().trim().email(), email: z
.string()
.trim()
.email()
.refine((val) => val === val.toLowerCase(), "Email must be lowercase"),
organizationId: z.string().trim(), organizationId: z.string().trim(),
code: z.string().trim() code: z.string().trim()
}), }),
@@ -46,6 +46,51 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
} }
}); });
server.route({
method: "GET",
url: "/duplicate-accounts",
config: {
rateLimit: readLimit
},
schema: {
response: {
200: z.object({
users: UsersSchema.extend({ isMyAccount: z.boolean() }).array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT], { requireOrg: false }),
handler: async (req) => {
if (req.auth.authMode === AuthMode.JWT && req.auth.user.email) {
const users = await server.services.user.getAllMyAccounts(req.auth.user.email, req.permission.id);
return { users };
}
return { users: [] };
}
});
server.route({
method: "POST",
url: "/remove-duplicate-accounts",
config: {
rateLimit: writeLimit
},
schema: {
response: {
200: z.object({
message: z.string()
})
}
},
onRequest: verifyAuth([AuthMode.JWT], { requireOrg: false }),
handler: async (req) => {
if (req.auth.authMode === AuthMode.JWT && req.auth.user.email) {
await server.services.user.removeMyDuplicateAccounts(req.auth.user.email, req.permission.id);
}
return { message: "Removed all duplicate accounts" };
}
});
server.route({ server.route({
method: "GET", method: "GET",
url: "/private-key", url: "/private-key",
@@ -27,8 +27,19 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
projectId: z.string().describe(PROJECT_USERS.INVITE_MEMBER.projectId) projectId: z.string().describe(PROJECT_USERS.INVITE_MEMBER.projectId)
}), }),
body: z.object({ body: z.object({
emails: z.string().email().array().default([]).describe(PROJECT_USERS.INVITE_MEMBER.emails), emails: z
usernames: z.string().array().default([]).describe(PROJECT_USERS.INVITE_MEMBER.usernames), .string()
.email()
.array()
.default([])
.describe(PROJECT_USERS.INVITE_MEMBER.emails)
.refine((val) => val.every((el) => el === el.toLowerCase()), "Email must be lowercase"),
usernames: z
.string()
.array()
.default([])
.describe(PROJECT_USERS.INVITE_MEMBER.usernames)
.refine((val) => val.every((el) => el === el.toLowerCase()), "Username must be lowercase"),
roleSlugs: z.string().array().min(1).optional().describe(PROJECT_USERS.INVITE_MEMBER.roleSlugs) roleSlugs: z.string().array().min(1).optional().describe(PROJECT_USERS.INVITE_MEMBER.roleSlugs)
}), }),
response: { response: {
@@ -92,7 +103,13 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
projectId: z.string().describe(PROJECT_USERS.REMOVE_MEMBER.projectId) projectId: z.string().describe(PROJECT_USERS.REMOVE_MEMBER.projectId)
}), }),
body: z.object({ body: z.object({
emails: z.string().email().array().default([]).describe(PROJECT_USERS.REMOVE_MEMBER.emails), emails: z
.string()
.email()
.array()
.default([])
.describe(PROJECT_USERS.REMOVE_MEMBER.emails)
.refine((val) => val.every((el) => el === el.toLowerCase()), "Email must be lowercase"),
usernames: z.string().array().default([]).describe(PROJECT_USERS.REMOVE_MEMBER.usernames) usernames: z.string().array().default([]).describe(PROJECT_USERS.REMOVE_MEMBER.usernames)
}), }),
response: { response: {
@@ -199,9 +199,12 @@ export const authLoginServiceFactory = ({
providerAuthToken, providerAuthToken,
clientPublicKey clientPublicKey
}: TLoginGenServerPublicKeyDTO) => { }: TLoginGenServerPublicKeyDTO) => {
const userEnc = await userDAL.findUserEncKeyByUsername({ // akhilmhdh: case sensitive email resolution
const usersByUsername = await userDAL.findUserEncKeyByUsername({
username: email username: email
}); });
const userEnc =
usersByUsername?.length > 1 ? usersByUsername.find((el) => el.username === email) : usersByUsername?.[0];
const serverCfg = await getServerCfg(); const serverCfg = await getServerCfg();
@@ -250,9 +253,12 @@ export const authLoginServiceFactory = ({
}: TLoginClientProofDTO) => { }: TLoginClientProofDTO) => {
const appCfg = getConfig(); const appCfg = getConfig();
const userEnc = await userDAL.findUserEncKeyByUsername({ // akhilmhdh: case sensitive email resolution
const usersByUsername = await userDAL.findUserEncKeyByUsername({
username: email username: email
}); });
const userEnc =
usersByUsername?.length > 1 ? usersByUsername.find((el) => el.username === email) : usersByUsername?.[0];
if (!userEnc) throw new Error("Failed to find user"); if (!userEnc) throw new Error("Failed to find user");
const user = await userDAL.findById(userEnc.userId); const user = await userDAL.findById(userEnc.userId);
const cfg = getConfig(); const cfg = getConfig();
@@ -649,10 +655,12 @@ export const authLoginServiceFactory = ({
* 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.findUserByUsername(email); // akhilmhdh: case sensitive email resolution
const usersByUsername = await userDAL.findUserByUsername(email);
let user = usersByUsername?.length > 1 ? usersByUsername.find((el) => el.username === email) : usersByUsername?.[0];
const serverCfg = await getServerCfg(); const serverCfg = await getServerCfg();
if (serverCfg.enabledLoginMethods) { if (serverCfg.enabledLoginMethods && user) {
switch (authMethod) { switch (authMethod) {
case AuthMethod.GITHUB: { case AuthMethod.GITHUB: {
if (!serverCfg.enabledLoginMethods.includes(LoginMethod.GITHUB)) { if (!serverCfg.enabledLoginMethods.includes(LoginMethod.GITHUB)) {
@@ -715,8 +723,8 @@ export const authLoginServiceFactory = ({
} }
user = await userDAL.create({ user = await userDAL.create({
username: email, username: email.toLowerCase(),
email, email: email.toLowerCase(),
isEmailVerified: true, isEmailVerified: true,
firstName, firstName,
lastName, lastName,
@@ -814,11 +822,14 @@ export const authLoginServiceFactory = ({
? decodedProviderToken.orgId ? decodedProviderToken.orgId
: undefined; : undefined;
const userEnc = await userDAL.findUserEncKeyByUsername({ // akhilmhdh: case sensitive email resolution
const usersByUsername = await userDAL.findUserEncKeyByUsername({
username: email username: email
}); });
if (!userEnc) throw new BadRequestError({ message: "Invalid token" }); const userEnc =
if (!userEnc.serverEncryptedPrivateKey) usersByUsername?.length > 1 ? usersByUsername.find((el) => el.username === email) : usersByUsername?.[0];
if (!userEnc?.serverEncryptedPrivateKey)
throw new BadRequestError({ message: "Key handoff incomplete. Please try logging in again." }); throw new BadRequestError({ message: "Key handoff incomplete. Please try logging in again." });
const token = await generateUserTokens({ const token = await generateUserTokens({
@@ -121,7 +121,10 @@ export const authPaswordServiceFactory = ({
*/ */
const sendPasswordResetEmail = async (email: string) => { const sendPasswordResetEmail = async (email: string) => {
const sendEmail = async () => { const sendEmail = async () => {
const user = await userDAL.findUserByUsername(email); const users = await userDAL.findUserByUsername(email);
// akhilmhdh: case sensitive email resolution
const user = users?.length > 1 ? users.find((el) => el.username === email) : users?.[0];
if (!user) throw new BadRequestError({ message: "Failed to find user data" });
if (user && user.isAccepted) { if (user && user.isAccepted) {
const cfg = getConfig(); const cfg = getConfig();
@@ -152,7 +155,10 @@ 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.findUserByUsername(email); const users = await userDAL.findUserByUsername(email);
// akhilmhdh: case sensitive email resolution
const user = users?.length > 1 ? users.find((el) => el.username === email) : users?.[0];
if (!user) throw new BadRequestError({ message: "Failed to find user data" });
const userEnc = await userDAL.findUserEncKeyByUserId(user.id); const userEnc = await userDAL.findUserEncKeyByUserId(user.id);
@@ -73,18 +73,27 @@ export const authSignupServiceFactory = ({
}: TAuthSignupDep) => { }: TAuthSignupDep) => {
// first step of signup. create user and send email // first step of signup. create user and send email
const beginEmailSignupProcess = async (email: string) => { const beginEmailSignupProcess = async (email: string) => {
const isEmailInvalid = await isDisposableEmail(email); const sanitizedEmail = email.toLowerCase();
const isEmailInvalid = await isDisposableEmail(sanitizedEmail);
if (isEmailInvalid) { if (isEmailInvalid) {
throw new Error("Provided a disposable email"); throw new Error("Provided a disposable email");
} }
let user = await userDAL.findUserByUsername(email); // akhilmhdh: case sensitive email resolution
const usersByUsername = await userDAL.findUserByUsername(sanitizedEmail);
let user =
usersByUsername?.length > 1 ? usersByUsername.find((el) => el.username === sanitizedEmail) : usersByUsername?.[0];
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 BadRequestError({ message: "Failed to send verification code for complete account" });
} }
if (!user) { if (!user) {
user = await userDAL.create({ authMethods: [AuthMethod.EMAIL], username: email, email, isGhost: false }); user = await userDAL.create({
authMethods: [AuthMethod.EMAIL],
username: sanitizedEmail,
email: sanitizedEmail,
isGhost: false
});
} }
if (!user) throw new Error("Failed to create user"); if (!user) throw new Error("Failed to create user");
@@ -96,7 +105,7 @@ export const authSignupServiceFactory = ({
await smtpService.sendMail({ await smtpService.sendMail({
template: SmtpTemplates.SignupEmailVerification, template: SmtpTemplates.SignupEmailVerification,
subjectLine: "Infisical confirmation code", subjectLine: "Infisical confirmation code",
recipients: [user.email as string], recipients: [sanitizedEmail],
substitutions: { substitutions: {
code: token code: token
} }
@@ -104,11 +113,15 @@ export const authSignupServiceFactory = ({
}; };
const verifyEmailSignup = async (email: string, code: string) => { const verifyEmailSignup = async (email: string, code: string) => {
const user = await userDAL.findUserByUsername(email); const sanitizedEmail = email.toLowerCase();
const usersByUsername = await userDAL.findUserByUsername(sanitizedEmail);
const user =
usersByUsername?.length > 1 ? usersByUsername.find((el) => el.username === sanitizedEmail) : usersByUsername?.[0];
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");
} }
const appCfg = getConfig(); const appCfg = getConfig();
await tokenService.validateTokenForUser({ await tokenService.validateTokenForUser({
type: TokenType.TOKEN_EMAIL_CONFIRMATION, type: TokenType.TOKEN_EMAIL_CONFIRMATION,
@@ -153,12 +166,15 @@ export const authSignupServiceFactory = ({
authorization, authorization,
useDefaultOrg useDefaultOrg
}: TCompleteAccountSignupDTO) => { }: TCompleteAccountSignupDTO) => {
const sanitizedEmail = email.toLowerCase();
const appCfg = getConfig(); const appCfg = getConfig();
const serverCfg = await getServerCfg(); const serverCfg = await getServerCfg();
const user = await userDAL.findOne({ username: email }); const usersByUsername = await userDAL.findUserByUsername(sanitizedEmail);
const user =
usersByUsername?.length > 1 ? usersByUsername.find((el) => el.username === sanitizedEmail) : usersByUsername?.[0];
if (!user || (user && user.isAccepted)) { if (!user || (user && user.isAccepted)) {
throw new Error("Failed to complete account for complete user"); throw new BadRequestError({ message: "Failed to complete account for complete user" });
} }
let organizationId: string | null = null; let organizationId: string | null = null;
@@ -315,7 +331,7 @@ export const authSignupServiceFactory = ({
} }
const updatedMembersips = await orgDAL.updateMembership( const updatedMembersips = await orgDAL.updateMembership(
{ inviteEmail: email, status: OrgMembershipStatus.Invited }, { inviteEmail: sanitizedEmail, status: OrgMembershipStatus.Invited },
{ userId: user.id, status: OrgMembershipStatus.Accepted } { userId: user.id, status: OrgMembershipStatus.Accepted }
); );
const uniqueOrgId = [...new Set(updatedMembersips.map(({ orgId }) => orgId))]; const uniqueOrgId = [...new Set(updatedMembersips.map(({ orgId }) => orgId))];
@@ -382,9 +398,9 @@ export const authSignupServiceFactory = ({
* User signup flow when they are invited to join the org * User signup flow when they are invited to join the org
* */ * */
const completeAccountInvite = async ({ const completeAccountInvite = async ({
email,
ip, ip,
salt, salt,
email,
password, password,
verifier, verifier,
firstName, firstName,
@@ -399,7 +415,10 @@ export const authSignupServiceFactory = ({
encryptedPrivateKeyTag, encryptedPrivateKeyTag,
authorization authorization
}: TCompleteAccountInviteDTO) => { }: TCompleteAccountInviteDTO) => {
const user = await userDAL.findUserByUsername(email); const sanitizedEmail = email.toLowerCase();
const usersByUsername = await userDAL.findUserByUsername(sanitizedEmail);
const user =
usersByUsername?.length > 1 ? usersByUsername.find((el) => el.username === sanitizedEmail) : usersByUsername?.[0];
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");
} }
@@ -407,7 +426,7 @@ export const authSignupServiceFactory = ({
validateSignUpAuthorization(authorization, user.id); validateSignUpAuthorization(authorization, user.id);
const [orgMembership] = await orgDAL.findMembership({ const [orgMembership] = await orgDAL.findMembership({
inviteEmail: email, inviteEmail: sanitizedEmail,
status: OrgMembershipStatus.Invited status: OrgMembershipStatus.Invited
}); });
if (!orgMembership) if (!orgMembership)
@@ -454,7 +473,7 @@ export const authSignupServiceFactory = ({
const serverGeneratedPrivateKey = await getUserPrivateKey(serverGeneratedPassword, { const serverGeneratedPrivateKey = await getUserPrivateKey(serverGeneratedPassword, {
...systemGeneratedUserEncryptionKey ...systemGeneratedUserEncryptionKey
}); });
const encKeys = await generateUserSrpKeys(email, password, { const encKeys = await generateUserSrpKeys(sanitizedEmail, password, {
publicKey: systemGeneratedUserEncryptionKey.publicKey, publicKey: systemGeneratedUserEncryptionKey.publicKey,
privateKey: serverGeneratedPrivateKey privateKey: serverGeneratedPrivateKey
}); });
@@ -505,7 +524,7 @@ export const authSignupServiceFactory = ({
} }
const updatedMembersips = await orgDAL.updateMembership( const updatedMembersips = await orgDAL.updateMembership(
{ inviteEmail: email, status: OrgMembershipStatus.Invited }, { inviteEmail: sanitizedEmail, status: OrgMembershipStatus.Invited },
{ userId: us.id, status: OrgMembershipStatus.Accepted }, { userId: us.id, status: OrgMembershipStatus.Accepted },
tx tx
); );
+9 -2
View File
@@ -811,7 +811,11 @@ export const orgServiceFactory = ({
const users: Pick<TUsers, "id" | "firstName" | "lastName" | "email" | "username">[] = []; const users: Pick<TUsers, "id" | "firstName" | "lastName" | "email" | "username">[] = [];
for await (const inviteeEmail of inviteeEmails) { for await (const inviteeEmail of inviteeEmails) {
let inviteeUser = await userDAL.findUserByUsername(inviteeEmail, tx); const usersByUsername = await userDAL.findUserByUsername(inviteeEmail, tx);
let inviteeUser =
usersByUsername?.length > 1
? usersByUsername.find((el) => el.username === inviteeEmail)
: usersByUsername?.[0];
// if the user doesn't exist we create the user with the email // if the user doesn't exist we create the user with the email
if (!inviteeUser) { if (!inviteeUser) {
@@ -1223,10 +1227,13 @@ 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.findUserByUsername(email); const usersByUsername = await userDAL.findUserByUsername(email);
const user =
usersByUsername?.length > 1 ? usersByUsername.find((el) => el.username === email) : usersByUsername?.[0];
if (!user) { if (!user) {
throw new NotFoundError({ message: "User not found" }); throw new NotFoundError({ message: "User not found" });
} }
const [orgMembership] = await orgDAL.findMembership({ const [orgMembership] = await orgDAL.findMembership({
[`${TableName.OrgMembership}.userId` as "userId"]: user.id, [`${TableName.OrgMembership}.userId` as "userId"]: user.id,
status: OrgMembershipStatus.Invited, status: OrgMembershipStatus.Invited,
@@ -257,8 +257,8 @@ export const superAdminServiceFactory = ({
const adminSignUp = async ({ const adminSignUp = async ({
lastName, lastName,
firstName, firstName,
salt,
email, email,
salt,
password, password,
verifier, verifier,
publicKey, publicKey,
@@ -272,7 +272,8 @@ export const superAdminServiceFactory = ({
userAgent userAgent
}: TAdminSignUpDTO) => { }: TAdminSignUpDTO) => {
const appCfg = getConfig(); const appCfg = getConfig();
const existingUser = await userDAL.findOne({ email }); const sanitizedEmail = email.toLowerCase();
const existingUser = await userDAL.findOne({ username: sanitizedEmail });
if (existingUser) throw new BadRequestError({ name: "Admin sign up", message: "User already exists" }); if (existingUser) throw new BadRequestError({ name: "Admin sign up", message: "User already exists" });
const privateKey = await getUserPrivateKey(password, { const privateKey = await getUserPrivateKey(password, {
@@ -292,8 +293,8 @@ export const superAdminServiceFactory = ({
{ {
firstName, firstName,
lastName, lastName,
username: email, username: sanitizedEmail,
email, email: sanitizedEmail,
superAdmin: true, superAdmin: true,
isGhost: false, isGhost: false,
isAccepted: true, isAccepted: true,
@@ -348,12 +349,13 @@ export const superAdminServiceFactory = ({
const bootstrapInstance = async ({ email, password, organizationName }: TAdminBootstrapInstanceDTO) => { const bootstrapInstance = async ({ email, password, organizationName }: TAdminBootstrapInstanceDTO) => {
const appCfg = getConfig(); const appCfg = getConfig();
const sanitizedEmail = email.toLowerCase();
const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID); const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID);
if (serverCfg?.initialized) { if (serverCfg?.initialized) {
throw new BadRequestError({ message: "Instance has already been set up" }); throw new BadRequestError({ message: "Instance has already been set up" });
} }
const existingUser = await userDAL.findOne({ email }); const existingUser = await userDAL.findOne({ email: sanitizedEmail });
if (existingUser) throw new BadRequestError({ name: "Instance initialization", message: "User already exists" }); if (existingUser) throw new BadRequestError({ name: "Instance initialization", message: "User already exists" });
const userInfo = await userDAL.transaction(async (tx) => { const userInfo = await userDAL.transaction(async (tx) => {
@@ -361,8 +363,8 @@ export const superAdminServiceFactory = ({
{ {
firstName: "Admin", firstName: "Admin",
lastName: "User", lastName: "User",
username: email, username: sanitizedEmail,
email, email: sanitizedEmail,
superAdmin: true, superAdmin: true,
isGhost: false, isGhost: false,
isAccepted: true, isAccepted: true,
@@ -372,7 +374,7 @@ export const superAdminServiceFactory = ({
tx tx
); );
const { tag, encoding, ciphertext, iv } = infisicalSymmetricEncypt(password); const { tag, encoding, ciphertext, iv } = infisicalSymmetricEncypt(password);
const encKeys = await generateUserSrpKeys(email, password); const encKeys = await generateUserSrpKeys(sanitizedEmail, password);
const userEnc = await userDAL.createUserEncryption( const userEnc = await userDAL.createUserEncryption(
{ {
+5 -5
View File
@@ -17,7 +17,8 @@ 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 findUserByUsername = async (username: string, tx?: Knex) => userOrm.findOne({ username }, tx); const findUserByUsername = async (username: string, tx?: Knex) =>
(tx || db)(TableName.Users).whereRaw('lower("username") = :username', { username: username.toLowerCase() });
const getUsersByFilter = async ({ const getUsersByFilter = async ({
limit, limit,
@@ -41,7 +42,7 @@ export const userDALFactory = (db: TDbClient) => {
.whereILike("email", `%${searchTerm}%`) .whereILike("email", `%${searchTerm}%`)
.orWhereILike("firstName", `%${searchTerm}%`) .orWhereILike("firstName", `%${searchTerm}%`)
.orWhereILike("lastName", `%${searchTerm}%`) .orWhereILike("lastName", `%${searchTerm}%`)
.orWhereLike("username", `%${searchTerm}%`); .orWhereRaw('lower("username") like ?', `%${searchTerm}%`);
}); });
} }
@@ -65,12 +66,11 @@ export const userDALFactory = (db: TDbClient) => {
try { try {
return await db return await db
.replicaNode()(TableName.Users) .replicaNode()(TableName.Users)
.whereRaw('lower("username") = :username', { username: username.toLowerCase() })
.where({ .where({
username,
isGhost: false isGhost: false
}) })
.join(TableName.UserEncryptionKey, `${TableName.Users}.id`, `${TableName.UserEncryptionKey}.userId`) .join(TableName.UserEncryptionKey, `${TableName.Users}.id`, `${TableName.UserEncryptionKey}.userId`);
.first();
} catch (error) { } catch (error) {
throw new DatabaseError({ error, name: "Find user enc by email" }); throw new DatabaseError({ error, name: "Find user enc by email" });
} }
+35 -8
View File
@@ -21,7 +21,7 @@ type TUserServiceFactoryDep = {
userDAL: Pick< userDAL: Pick<
TUserDALFactory, TUserDALFactory,
| "find" | "find"
| "findOne" | "findUserByUsername"
| "findById" | "findById"
| "transaction" | "transaction"
| "updateById" | "updateById"
@@ -54,8 +54,11 @@ export const userServiceFactory = ({
permissionService permissionService
}: TUserServiceFactoryDep) => { }: TUserServiceFactoryDep) => {
const sendEmailVerificationCode = async (username: string) => { const sendEmailVerificationCode = async (username: string) => {
const user = await userDAL.findOne({ username }); // akhilmhdh: case sensitive email resolution
const users = await userDAL.findUserByUsername(username);
const user = users?.length > 1 ? users.find((el) => el.username === username) : users?.[0];
if (!user) throw new NotFoundError({ name: `User with username '${username}' not found` }); if (!user) throw new NotFoundError({ name: `User with username '${username}' not found` });
if (!user.email) if (!user.email)
throw new BadRequestError({ name: "Failed to send email verification code due to no email on user" }); throw new BadRequestError({ name: "Failed to send email verification code due to no email on user" });
if (user.isEmailVerified) if (user.isEmailVerified)
@@ -77,7 +80,10 @@ export const userServiceFactory = ({
}; };
const verifyEmailVerificationCode = async (username: string, code: string) => { const verifyEmailVerificationCode = async (username: string, code: string) => {
const user = await userDAL.findOne({ username }); // akhilmhdh: case sensitive email resolution
const usersByusername = await userDAL.findUserByUsername(username);
const user =
usersByusername?.length > 1 ? usersByusername.find((el) => el.username === username) : usersByusername?.[0];
if (!user) throw new NotFoundError({ name: `User with username '${username}' not found` }); if (!user) throw new NotFoundError({ name: `User with username '${username}' not found` });
if (!user.email) if (!user.email)
throw new BadRequestError({ name: "Failed to verify email verification code due to no email on user" }); throw new BadRequestError({ name: "Failed to verify email verification code due to no email on user" });
@@ -104,7 +110,7 @@ export const userServiceFactory = ({
// check if there are verified users with the same email. // check if there are verified users with the same email.
const users = await userDAL.find( const users = await userDAL.find(
{ {
email, email: email.toLowerCase(),
isEmailVerified: true isEmailVerified: true
}, },
{ tx } { tx }
@@ -163,7 +169,7 @@ export const userServiceFactory = ({
await userDAL.updateById( await userDAL.updateById(
user.id, user.id,
{ {
username: email username: email.toLowerCase()
}, },
tx tx
); );
@@ -212,6 +218,23 @@ export const userServiceFactory = ({
return updatedUser; return updatedUser;
}; };
const getAllMyAccounts = async (email: string, userId: string) => {
const users = await userDAL.find({ email });
return users?.map((el) => ({ ...el, isMyAccount: el.id === userId }));
};
const removeMyDuplicateAccounts = async (email: string, userId: string) => {
const users = await userDAL.find({ email });
const duplicatedAccounts = users?.filter((el) => el.id !== userId);
const myAccount = users?.find((el) => el.id === userId);
if (duplicatedAccounts.length && myAccount) {
await userDAL.transaction(async (tx) => {
await userDAL.delete({ $in: { id: duplicatedAccounts?.map((el) => el.id) } }, tx);
await userDAL.updateById(userId, { username: myAccount.username.toLowerCase() });
});
}
};
const getMe = async (userId: string) => { const getMe = async (userId: string) => {
const user = await userDAL.findUserEncKeyByUserId(userId); const user = await userDAL.findUserEncKeyByUserId(userId);
if (!user) throw new NotFoundError({ message: `User with ID '${userId}' not found`, name: "GetMe" }); if (!user) throw new NotFoundError({ message: `User with ID '${userId}' not found`, name: "GetMe" });
@@ -313,9 +336,11 @@ export const userServiceFactory = ({
}; };
const listUserGroups = async ({ username, actorOrgId, actor, actorId, actorAuthMethod }: TListUserGroupsDTO) => { const listUserGroups = async ({ username, actorOrgId, actor, actorId, actorAuthMethod }: TListUserGroupsDTO) => {
const user = await userDAL.findOne({ // akhilmhdh: case sensitive email resolution
username const usersByusername = await userDAL.findUserByUsername(username);
}); const user =
usersByusername?.length > 1 ? usersByusername.find((el) => el.username === username) : usersByusername?.[0];
if (!user) throw new NotFoundError({ name: `User with username '${username}' not found` });
// This makes it so the user can always read information about themselves, but no one else if they don't have the Members Read permission. // This makes it so the user can always read information about themselves, but no one else if they don't have the Members Read permission.
if (user.id !== actorId) { if (user.id !== actorId) {
@@ -346,7 +371,9 @@ export const userServiceFactory = ({
getUserAction, getUserAction,
unlockUser, unlockUser,
getUserPrivateKey, getUserPrivateKey,
getAllMyAccounts,
getUserProjectFavorites, getUserProjectFavorites,
removeMyDuplicateAccounts,
updateUserProjectFavorites updateUserProjectFavorites
}; };
}; };