feat: implemented backend updates for email casing issue

This commit is contained in:
=
2025-04-30 23:51:55 +05:30
parent 6188de43e4
commit f85add7cca
19 changed files with 304 additions and 89 deletions

View File

@@ -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
`);
}
}

View File

@@ -111,9 +111,9 @@ export const groupDALFactory = (db: TDbClient) => {
}
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) {
void query.andWhere(`${TableName.Users}.username`, "ilike", `%${username}%`);
void query.andWhereRaw(`lower("${TableName.Users}"."username") ilike ?`, `%${username}%`);
}
switch (filter) {

View File

@@ -30,7 +30,7 @@ import {
import { TUserGroupMembershipDALFactory } from "./user-group-membership-dal";
type TGroupServiceFactoryDep = {
userDAL: Pick<TUserDALFactory, "find" | "findUserEncKeyByUserIdsBatch" | "transaction" | "findOne">;
userDAL: Pick<TUserDALFactory, "find" | "findUserEncKeyByUserIdsBatch" | "transaction" | "findUserByUsername">;
groupDAL: Pick<
TGroupDALFactory,
"create" | "findOne" | "update" | "delete" | "findAllGroupPossibleMembers" | "findById" | "transaction"
@@ -380,7 +380,10 @@ export const groupServiceFactory = ({
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}` });
const users = await addUsersToGroupByUserIds({
@@ -461,7 +464,10 @@ export const groupServiceFactory = ({
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}` });
const users = await removeUsersFromGroupByUserIds({

View File

@@ -380,7 +380,7 @@ export const ldapConfigServiceFactory = ({
if (serverCfg.trustLdapEmails) {
newUser = await userDAL.findOne(
{
email,
email: email.toLowerCase(),
isEmailVerified: true
},
tx
@@ -391,8 +391,8 @@ export const ldapConfigServiceFactory = ({
const uniqueUsername = await normalizeUsername(username, userDAL);
newUser = await userDAL.create(
{
username: serverCfg.trustLdapEmails ? email : uniqueUsername,
email,
username: serverCfg.trustLdapEmails ? email.toLowerCase() : uniqueUsername,
email: email.toLowerCase(),
isEmailVerified: serverCfg.trustLdapEmails,
firstName,
lastName,
@@ -429,7 +429,7 @@ export const ldapConfigServiceFactory = ({
await orgMembershipDAL.create(
{
userId: newUser.id,
inviteEmail: email,
inviteEmail: email.toLowerCase(),
orgId,
role,
roleId,

View File

@@ -30,7 +30,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
auditLogsRetentionDays: 0,
auditLogStreams: false,
auditLogStreamLimit: 3,
samlSSO: false,
samlSSO: true,
hsm: false,
oidcSSO: false,
scim: false,

View File

@@ -171,8 +171,8 @@ export const oidcConfigServiceFactory = ({
};
const oidcLogin = async ({
externalId,
email,
externalId,
firstName,
lastName,
orgId,
@@ -181,6 +181,7 @@ export const oidcConfigServiceFactory = ({
manageGroupMemberships
}: TOidcLoginDTO) => {
const serverCfg = await getServerCfg();
const sanitizedEmail = email.toLowerCase();
if (serverCfg.enabledLoginMethods && !serverCfg.enabledLoginMethods.includes(LoginMethod.OIDC)) {
throw new ForbiddenRequestError({
@@ -215,7 +216,7 @@ export const oidcConfigServiceFactory = ({
await orgMembershipDAL.create(
{
userId: userAlias.userId,
inviteEmail: email,
inviteEmail: sanitizedEmail,
orgId,
role,
roleId,
@@ -245,7 +246,7 @@ export const oidcConfigServiceFactory = ({
// we prioritize getting the most complete user to create the new alias under
newUser = await userDAL.findOne(
{
email,
email: sanitizedEmail,
isEmailVerified: true
},
tx
@@ -255,7 +256,7 @@ export const oidcConfigServiceFactory = ({
// this fetches user entries created via invites
newUser = await userDAL.findOne(
{
username: email
username: sanitizedEmail
},
tx
);
@@ -273,10 +274,10 @@ export const oidcConfigServiceFactory = ({
const uniqueUsername = await normalizeUsername(externalId, userDAL);
newUser = await userDAL.create(
{
email,
email: sanitizedEmail,
firstName,
isEmailVerified: serverCfg.trustOidcEmails,
username: serverCfg.trustOidcEmails ? email : uniqueUsername,
username: serverCfg.trustOidcEmails ? sanitizedEmail : uniqueUsername,
lastName,
authMethods: [],
isGhost: false
@@ -290,7 +291,7 @@ export const oidcConfigServiceFactory = ({
userId: newUser.id,
aliasType: UserAliasType.OIDC,
externalId,
emails: email ? [email] : [],
emails: sanitizedEmail ? [sanitizedEmail] : [],
orgId
},
tx
@@ -310,7 +311,7 @@ export const oidcConfigServiceFactory = ({
await orgMembershipDAL.create(
{
userId: newUser.id,
inviteEmail: email,
inviteEmail: sanitizedEmail,
orgId,
role,
roleId,

View File

@@ -266,7 +266,7 @@ export const samlConfigServiceFactory = ({
await orgMembershipDAL.create(
{
userId: userAlias.userId,
inviteEmail: email,
inviteEmail: email.toLowerCase(),
orgId,
role,
roleId,
@@ -324,7 +324,7 @@ export const samlConfigServiceFactory = ({
if (serverCfg.trustSamlEmails) {
newUser = await userDAL.findOne(
{
email,
email: email.toLowerCase(),
isEmailVerified: true
},
tx
@@ -335,8 +335,8 @@ export const samlConfigServiceFactory = ({
const uniqueUsername = await normalizeUsername(`${firstName ?? ""}-${lastName ?? ""}`, userDAL);
newUser = await userDAL.create(
{
username: serverCfg.trustSamlEmails ? email : uniqueUsername,
email,
username: serverCfg.trustSamlEmails ? email.toLowerCase() : uniqueUsername,
email: email.toLowerCase(),
isEmailVerified: serverCfg.trustSamlEmails,
firstName,
lastName,
@@ -352,7 +352,7 @@ export const samlConfigServiceFactory = ({
userId: newUser.id,
aliasType: UserAliasType.SAML,
externalId,
emails: email ? [email] : [],
emails: email ? [email.toLowerCase()] : [],
orgId
},
tx
@@ -372,7 +372,7 @@ export const samlConfigServiceFactory = ({
await orgMembershipDAL.create(
{
userId: newUser.id,
inviteEmail: email,
inviteEmail: email.toLowerCase(),
orgId,
role,
roleId,

View File

@@ -342,7 +342,7 @@ export const scimServiceFactory = ({
orgMembership = await orgMembershipDAL.create(
{
userId: userAlias.userId,
inviteEmail: email,
inviteEmail: email.toLowerCase(),
orgId,
role,
roleId,
@@ -364,7 +364,7 @@ export const scimServiceFactory = ({
if (trustScimEmails) {
user = await userDAL.findOne(
{
email,
email: email.toLowerCase(),
isEmailVerified: true
},
tx
@@ -379,8 +379,8 @@ export const scimServiceFactory = ({
);
user = await userDAL.create(
{
username: trustScimEmails ? email : uniqueUsername,
email,
username: trustScimEmails ? email.toLowerCase() : uniqueUsername,
email: email.toLowerCase(),
isEmailVerified: trustScimEmails,
firstName,
lastName,
@@ -396,7 +396,7 @@ export const scimServiceFactory = ({
userId: user.id,
aliasType,
externalId,
emails: email ? [email] : [],
emails: email ? [email.toLowerCase()] : [],
orgId
},
tx
@@ -418,7 +418,7 @@ export const scimServiceFactory = ({
orgMembership = await orgMembershipDAL.create(
{
userId: user.id,
inviteEmail: email,
inviteEmail: email.toLowerCase(),
orgId,
role,
roleId,
@@ -529,7 +529,7 @@ export const scimServiceFactory = ({
membership.userId,
{
firstName: scimUser.name.givenName,
email: scimUser.emails[0].value,
email: scimUser.emails[0].value.toLowerCase(),
lastName: scimUser.name.familyName,
isEmailVerified: hasEmailChanged ? trustScimEmails : undefined
},
@@ -606,7 +606,7 @@ export const scimServiceFactory = ({
membership.userId,
{
firstName,
email,
email: email?.toLowerCase(),
lastName,
isEmailVerified:
org.orgAuthMethod === OrgAuthMethod.OIDC ? serverCfg.trustOidcEmails : serverCfg.trustSamlEmails

View File

@@ -1,6 +1,8 @@
import { Knex } from "knex";
import { Compare, Filter, parse } from "scim2-parse-filter";
import { TableName } from "@app/db/schemas";
const appendParentToGroupingOperator = (parentPath: string, filter: Filter) => {
if (filter.op !== "[]" && filter.op !== "and" && filter.op !== "or" && filter.op !== "not") {
return { ...filter, attrPath: `${parentPath}.${(filter as Compare).attrPath}` };
@@ -27,8 +29,12 @@ const processDynamicQuery = (
const { scimFilterAst, query } = stack.pop()!;
switch (scimFilterAst.op) {
case "eq": {
let sanitizedValue = scimFilterAst.compValue;
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;
}
case "pr": {
@@ -62,18 +68,30 @@ const processDynamicQuery = (
break;
}
case "ew": {
let sanitizedValue = scimFilterAst.compValue;
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;
}
case "co": {
let sanitizedValue = scimFilterAst.compValue;
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;
}
case "ne": {
let sanitizedValue = scimFilterAst.compValue;
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;
}
case "and": {

View File

@@ -16,7 +16,12 @@ export const registerInviteOrgRouter = async (server: FastifyZodProvider) => {
method: "POST",
schema: {
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(),
projects: z
.object({
@@ -115,7 +120,11 @@ export const registerInviteOrgRouter = async (server: FastifyZodProvider) => {
},
schema: {
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(),
code: z.string().trim()
}),

View File

@@ -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({
method: "GET",
url: "/private-key",

View File

@@ -27,8 +27,19 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
projectId: z.string().describe(PROJECT_USERS.INVITE_MEMBER.projectId)
}),
body: z.object({
emails: z.string().email().array().default([]).describe(PROJECT_USERS.INVITE_MEMBER.emails),
usernames: z.string().array().default([]).describe(PROJECT_USERS.INVITE_MEMBER.usernames),
emails: z
.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)
}),
response: {
@@ -92,7 +103,13 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
projectId: z.string().describe(PROJECT_USERS.REMOVE_MEMBER.projectId)
}),
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)
}),
response: {

View File

@@ -199,9 +199,12 @@ export const authLoginServiceFactory = ({
providerAuthToken,
clientPublicKey
}: TLoginGenServerPublicKeyDTO) => {
const userEnc = await userDAL.findUserEncKeyByUsername({
// akhilmhdh: case sensitive email resolution
const usersByUsername = await userDAL.findUserEncKeyByUsername({
username: email
});
const userEnc =
usersByUsername?.length > 1 ? usersByUsername.find((el) => el.username === email) : usersByUsername?.[0];
const serverCfg = await getServerCfg();
@@ -250,9 +253,12 @@ export const authLoginServiceFactory = ({
}: TLoginClientProofDTO) => {
const appCfg = getConfig();
const userEnc = await userDAL.findUserEncKeyByUsername({
// akhilmhdh: case sensitive email resolution
const usersByUsername = await userDAL.findUserEncKeyByUsername({
username: email
});
const userEnc =
usersByUsername?.length > 1 ? usersByUsername.find((el) => el.username === email) : usersByUsername?.[0];
if (!userEnc) throw new Error("Failed to find user");
const user = await userDAL.findById(userEnc.userId);
const cfg = getConfig();
@@ -649,10 +655,12 @@ export const authLoginServiceFactory = ({
* OAuth2 login for google,github, and other oauth2 provider
* */
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();
if (serverCfg.enabledLoginMethods) {
if (serverCfg.enabledLoginMethods && user) {
switch (authMethod) {
case AuthMethod.GITHUB: {
if (!serverCfg.enabledLoginMethods.includes(LoginMethod.GITHUB)) {
@@ -715,8 +723,8 @@ export const authLoginServiceFactory = ({
}
user = await userDAL.create({
username: email,
email,
username: email.toLowerCase(),
email: email.toLowerCase(),
isEmailVerified: true,
firstName,
lastName,
@@ -814,11 +822,14 @@ export const authLoginServiceFactory = ({
? decodedProviderToken.orgId
: undefined;
const userEnc = await userDAL.findUserEncKeyByUsername({
// akhilmhdh: case sensitive email resolution
const usersByUsername = await userDAL.findUserEncKeyByUsername({
username: email
});
if (!userEnc) throw new BadRequestError({ message: "Invalid token" });
if (!userEnc.serverEncryptedPrivateKey)
const userEnc =
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." });
const token = await generateUserTokens({

View File

@@ -121,7 +121,10 @@ export const authPaswordServiceFactory = ({
*/
const sendPasswordResetEmail = async (email: string) => {
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) {
const cfg = getConfig();
@@ -152,7 +155,10 @@ export const authPaswordServiceFactory = ({
* */
const verifyPasswordResetEmail = async (email: string, code: string) => {
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);

View File

@@ -73,18 +73,27 @@ export const authSignupServiceFactory = ({
}: TAuthSignupDep) => {
// first step of signup. create user and send email
const beginEmailSignupProcess = async (email: string) => {
const isEmailInvalid = await isDisposableEmail(email);
const sanitizedEmail = email.toLowerCase();
const isEmailInvalid = await isDisposableEmail(sanitizedEmail);
if (isEmailInvalid) {
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) {
// 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) {
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");
@@ -96,7 +105,7 @@ export const authSignupServiceFactory = ({
await smtpService.sendMail({
template: SmtpTemplates.SignupEmailVerification,
subjectLine: "Infisical confirmation code",
recipients: [user.email as string],
recipients: [sanitizedEmail],
substitutions: {
code: token
}
@@ -104,11 +113,15 @@ export const authSignupServiceFactory = ({
};
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)) {
// 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");
}
const appCfg = getConfig();
await tokenService.validateTokenForUser({
type: TokenType.TOKEN_EMAIL_CONFIRMATION,
@@ -153,12 +166,15 @@ export const authSignupServiceFactory = ({
authorization,
useDefaultOrg
}: TCompleteAccountSignupDTO) => {
const sanitizedEmail = email.toLowerCase();
const appCfg = getConfig();
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)) {
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;
@@ -315,7 +331,7 @@ export const authSignupServiceFactory = ({
}
const updatedMembersips = await orgDAL.updateMembership(
{ inviteEmail: email, status: OrgMembershipStatus.Invited },
{ inviteEmail: sanitizedEmail, status: OrgMembershipStatus.Invited },
{ userId: user.id, status: OrgMembershipStatus.Accepted }
);
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
* */
const completeAccountInvite = async ({
email,
ip,
salt,
email,
password,
verifier,
firstName,
@@ -399,7 +415,10 @@ export const authSignupServiceFactory = ({
encryptedPrivateKeyTag,
authorization
}: 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)) {
throw new Error("Failed to complete account for complete user");
}
@@ -407,7 +426,7 @@ export const authSignupServiceFactory = ({
validateSignUpAuthorization(authorization, user.id);
const [orgMembership] = await orgDAL.findMembership({
inviteEmail: email,
inviteEmail: sanitizedEmail,
status: OrgMembershipStatus.Invited
});
if (!orgMembership)
@@ -454,7 +473,7 @@ export const authSignupServiceFactory = ({
const serverGeneratedPrivateKey = await getUserPrivateKey(serverGeneratedPassword, {
...systemGeneratedUserEncryptionKey
});
const encKeys = await generateUserSrpKeys(email, password, {
const encKeys = await generateUserSrpKeys(sanitizedEmail, password, {
publicKey: systemGeneratedUserEncryptionKey.publicKey,
privateKey: serverGeneratedPrivateKey
});
@@ -505,7 +524,7 @@ export const authSignupServiceFactory = ({
}
const updatedMembersips = await orgDAL.updateMembership(
{ inviteEmail: email, status: OrgMembershipStatus.Invited },
{ inviteEmail: sanitizedEmail, status: OrgMembershipStatus.Invited },
{ userId: us.id, status: OrgMembershipStatus.Accepted },
tx
);

View File

@@ -811,7 +811,11 @@ export const orgServiceFactory = ({
const users: Pick<TUsers, "id" | "firstName" | "lastName" | "email" | "username">[] = [];
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 (!inviteeUser) {
@@ -1223,10 +1227,13 @@ export const orgServiceFactory = ({
* magic link and issue a temporary signup token for user to complete setting up their account
*/
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) {
throw new NotFoundError({ message: "User not found" });
}
const [orgMembership] = await orgDAL.findMembership({
[`${TableName.OrgMembership}.userId` as "userId"]: user.id,
status: OrgMembershipStatus.Invited,

View File

@@ -257,8 +257,8 @@ export const superAdminServiceFactory = ({
const adminSignUp = async ({
lastName,
firstName,
salt,
email,
salt,
password,
verifier,
publicKey,
@@ -272,7 +272,8 @@ export const superAdminServiceFactory = ({
userAgent
}: TAdminSignUpDTO) => {
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" });
const privateKey = await getUserPrivateKey(password, {
@@ -292,8 +293,8 @@ export const superAdminServiceFactory = ({
{
firstName,
lastName,
username: email,
email,
username: sanitizedEmail,
email: sanitizedEmail,
superAdmin: true,
isGhost: false,
isAccepted: true,
@@ -348,12 +349,13 @@ export const superAdminServiceFactory = ({
const bootstrapInstance = async ({ email, password, organizationName }: TAdminBootstrapInstanceDTO) => {
const appCfg = getConfig();
const sanitizedEmail = email.toLowerCase();
const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID);
if (serverCfg?.initialized) {
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" });
const userInfo = await userDAL.transaction(async (tx) => {
@@ -361,8 +363,8 @@ export const superAdminServiceFactory = ({
{
firstName: "Admin",
lastName: "User",
username: email,
email,
username: sanitizedEmail,
email: sanitizedEmail,
superAdmin: true,
isGhost: false,
isAccepted: true,
@@ -372,7 +374,7 @@ export const superAdminServiceFactory = ({
tx
);
const { tag, encoding, ciphertext, iv } = infisicalSymmetricEncypt(password);
const encKeys = await generateUserSrpKeys(email, password);
const encKeys = await generateUserSrpKeys(sanitizedEmail, password);
const userEnc = await userDAL.createUserEncryption(
{

View File

@@ -17,7 +17,8 @@ export type TUserDALFactory = ReturnType<typeof userDALFactory>;
export const userDALFactory = (db: TDbClient) => {
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 ({
limit,
@@ -41,7 +42,7 @@ export const userDALFactory = (db: TDbClient) => {
.whereILike("email", `%${searchTerm}%`)
.orWhereILike("firstName", `%${searchTerm}%`)
.orWhereILike("lastName", `%${searchTerm}%`)
.orWhereLike("username", `%${searchTerm}%`);
.orWhereRaw('lower("username") like ?', `%${searchTerm}%`);
});
}
@@ -65,12 +66,11 @@ export const userDALFactory = (db: TDbClient) => {
try {
return await db
.replicaNode()(TableName.Users)
.whereRaw('lower("username") = :username', { username: username.toLowerCase() })
.where({
username,
isGhost: false
})
.join(TableName.UserEncryptionKey, `${TableName.Users}.id`, `${TableName.UserEncryptionKey}.userId`)
.first();
.join(TableName.UserEncryptionKey, `${TableName.Users}.id`, `${TableName.UserEncryptionKey}.userId`);
} catch (error) {
throw new DatabaseError({ error, name: "Find user enc by email" });
}

View File

@@ -21,7 +21,7 @@ type TUserServiceFactoryDep = {
userDAL: Pick<
TUserDALFactory,
| "find"
| "findOne"
| "findUserByUsername"
| "findById"
| "transaction"
| "updateById"
@@ -54,8 +54,11 @@ export const userServiceFactory = ({
permissionService
}: TUserServiceFactoryDep) => {
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.email)
throw new BadRequestError({ name: "Failed to send email verification code due to no email on user" });
if (user.isEmailVerified)
@@ -77,7 +80,10 @@ export const userServiceFactory = ({
};
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.email)
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.
const users = await userDAL.find(
{
email,
email: email.toLowerCase(),
isEmailVerified: true
},
{ tx }
@@ -163,7 +169,7 @@ export const userServiceFactory = ({
await userDAL.updateById(
user.id,
{
username: email
username: email.toLowerCase()
},
tx
);
@@ -212,6 +218,23 @@ export const userServiceFactory = ({
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 user = await userDAL.findUserEncKeyByUserId(userId);
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 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` });
// 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) {
@@ -346,7 +371,9 @@ export const userServiceFactory = ({
getUserAction,
unlockUser,
getUserPrivateKey,
getAllMyAccounts,
getUserProjectFavorites,
removeMyDuplicateAccounts,
updateUserProjectFavorites
};
};