Add user aliases concept and weave LDAP into it

This commit is contained in:
Tuan Dang
2024-03-06 12:06:40 -08:00
parent 327c5e2429
commit 76bd85efa7
39 changed files with 206 additions and 102 deletions

View File

@@ -164,6 +164,9 @@ import {
TUserActions, TUserActions,
TUserActionsInsert, TUserActionsInsert,
TUserActionsUpdate, TUserActionsUpdate,
TUserAliases,
TUserAliasesInsert,
TUserAliasesUpdate,
TUserEncryptionKeys, TUserEncryptionKeys,
TUserEncryptionKeysInsert, TUserEncryptionKeysInsert,
TUserEncryptionKeysUpdate, TUserEncryptionKeysUpdate,
@@ -178,6 +181,7 @@ import {
declare module "knex/types/tables" { declare module "knex/types/tables" {
interface Tables { interface Tables {
[TableName.Users]: Knex.CompositeTableType<TUsers, TUsersInsert, TUsersUpdate>; [TableName.Users]: Knex.CompositeTableType<TUsers, TUsersInsert, TUsersUpdate>;
[TableName.UserAliases]: Knex.CompositeTableType<TUserAliases, TUserAliasesInsert, TUserAliasesUpdate>;
[TableName.UserEncryptionKey]: Knex.CompositeTableType< [TableName.UserEncryptionKey]: Knex.CompositeTableType<
TUserEncryptionKeys, TUserEncryptionKeys,
TUserEncryptionKeysInsert, TUserEncryptionKeysInsert,

View File

@@ -25,24 +25,38 @@ export async function up(knex: Knex): Promise<void> {
}); });
} }
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) => { await knex.schema.alterTable(TableName.Users, (t) => {
t.string("username").notNullable(); t.string("username").unique().notNullable();
t.uuid("orgId").nullable();
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
t.string("email").nullable().alter(); t.string("email").nullable().alter();
t.unique(["username", "orgId"]);
}); });
await knex(TableName.Users).update("username", knex.ref("email")); await knex(TableName.Users).update("username", knex.ref("email"));
await createOnUpdateTrigger(knex, TableName.LdapConfig);
} }
export async function down(knex: Knex): Promise<void> { export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists(TableName.LdapConfig); await knex.schema.dropTableIfExists(TableName.LdapConfig);
await knex.schema.dropTableIfExists(TableName.UserAliases);
await knex.schema.alterTable(TableName.Users, (t) => { await knex.schema.alterTable(TableName.Users, (t) => {
t.dropColumn("username"); t.dropColumn("username");
t.dropColumn("orgId");
// t.string("email").notNullable().alter(); // t.string("email").notNullable().alter();
}); });
await dropOnUpdateTrigger(knex, TableName.LdapConfig); await dropOnUpdateTrigger(knex, TableName.LdapConfig);

View File

@@ -53,6 +53,7 @@ export * from "./service-tokens";
export * from "./super-admin"; export * from "./super-admin";
export * from "./trusted-ips"; export * from "./trusted-ips";
export * from "./user-actions"; export * from "./user-actions";
export * from "./user-aliases";
export * from "./user-encryption-keys"; export * from "./user-encryption-keys";
export * from "./users"; export * from "./users";
export * from "./webhooks"; export * from "./webhooks";

View File

@@ -27,5 +27,5 @@ export const LdapConfigsSchema = z.object({
}); });
export type TLdapConfigs = z.infer<typeof LdapConfigsSchema>; export type TLdapConfigs = z.infer<typeof LdapConfigsSchema>;
export type TLdapConfigsInsert = Omit<TLdapConfigs, TImmutableDBKeys>; export type TLdapConfigsInsert = Omit<z.input<typeof LdapConfigsSchema>, TImmutableDBKeys>;
export type TLdapConfigsUpdate = Partial<Omit<TLdapConfigs, TImmutableDBKeys>>; export type TLdapConfigsUpdate = Partial<Omit<z.input<typeof LdapConfigsSchema>, TImmutableDBKeys>>;

View File

@@ -2,6 +2,7 @@ import { z } from "zod";
export enum TableName { export enum TableName {
Users = "users", Users = "users",
UserAliases = "user_aliases",
UserEncryptionKey = "user_encryption_keys", UserEncryptionKey = "user_encryption_keys",
AuthTokens = "auth_tokens", AuthTokens = "auth_tokens",
AuthTokenSession = "auth_token_sessions", AuthTokenSession = "auth_token_sessions",

View File

@@ -0,0 +1,24 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const UserAliasesSchema = z.object({
id: z.string().uuid(),
userId: z.string().uuid(),
username: z.string(),
aliasType: z.string(),
externalId: z.string(),
emails: z.string().array().nullable().optional(),
orgId: z.string().uuid().nullable().optional(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TUserAliases = z.infer<typeof UserAliasesSchema>;
export type TUserAliasesInsert = Omit<z.input<typeof UserAliasesSchema>, TImmutableDBKeys>;
export type TUserAliasesUpdate = Partial<Omit<z.input<typeof UserAliasesSchema>, TImmutableDBKeys>>;

View File

@@ -21,8 +21,7 @@ export const UsersSchema = z.object({
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date(), updatedAt: z.date(),
isGhost: z.boolean().default(false), isGhost: z.boolean().default(false),
username: z.string(), username: z.string()
orgId: z.string().uuid().nullable().optional()
}); });
export type TUsers = z.infer<typeof UsersSchema>; export type TUsers = z.infer<typeof UsersSchema>;

View File

@@ -34,9 +34,11 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => {
async (req: IncomingMessage, user, cb) => { async (req: IncomingMessage, user, cb) => {
try { try {
const { isUserCompleted, providerAuthToken } = await server.services.ldap.ldapLogin({ const { isUserCompleted, providerAuthToken } = await server.services.ldap.ldapLogin({
externalId: user.uidNumber,
username: user.uid, username: user.uid,
firstName: user.givenName, firstName: user.givenName,
lastName: user.sn, lastName: user.sn,
emails: user.mail ? [user.mail] : [],
relayState: ((req as unknown as FastifyRequest).body as { RelayState?: string }).RelayState, relayState: ((req as unknown as FastifyRequest).body as { RelayState?: string }).RelayState,
orgId: (req as unknown as FastifyRequest).ldapConfig.organization orgId: (req as unknown as FastifyRequest).ldapConfig.organization
}); });

View File

@@ -237,7 +237,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
const user = await req.server.services.scim.createScimUser({ const user = await req.server.services.scim.createScimUser({
username: req.body.userName, username: req.body.userName,
email: primaryEmail as string, email: primaryEmail,
firstName: req.body.name.givenName, firstName: req.body.name.givenName,
lastName: req.body.name.familyName, lastName: req.body.name.familyName,
orgId: req.permission.orgId as string orgId: req.permission.orgId as string

View File

@@ -19,6 +19,8 @@ import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type";
import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal"; import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal";
import { TOrgDALFactory } from "@app/services/org/org-dal"; import { TOrgDALFactory } from "@app/services/org/org-dal";
import { TUserDALFactory } from "@app/services/user/user-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 { TLicenseServiceFactory } from "../license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission"; import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
@@ -34,6 +36,7 @@ type TLdapConfigServiceFactoryDep = {
>; >;
orgBotDAL: Pick<TOrgBotDALFactory, "findOne" | "create" | "transaction">; orgBotDAL: Pick<TOrgBotDALFactory, "findOne" | "create" | "transaction">;
userDAL: Pick<TUserDALFactory, "create" | "findOne" | "transaction" | "updateById">; userDAL: Pick<TUserDALFactory, "create" | "findOne" | "transaction" | "updateById">;
userAliasDAL: Pick<TUserAliasDALFactory, "create" | "findOne">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">; permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
}; };
@@ -45,6 +48,7 @@ export const ldapConfigServiceFactory = ({
orgDAL, orgDAL,
orgBotDAL, orgBotDAL,
userDAL, userDAL,
userAliasDAL,
permissionService, permissionService,
licenseService licenseService
}: TLdapConfigServiceFactoryDep) => { }: TLdapConfigServiceFactoryDep) => {
@@ -289,6 +293,8 @@ export const ldapConfigServiceFactory = ({
const boot = async () => { const boot = async () => {
try { try {
const organization = await orgDAL.findOne({ slug: organizationSlug }); const organization = await orgDAL.findOne({ slug: organizationSlug });
if (!organization) throw new BadRequestError({ message: "Org not found" });
const ldapConfig = await getLdapCfg({ const ldapConfig = await getLdapCfg({
orgId: organization.id, orgId: organization.id,
isActive: true isActive: true
@@ -302,7 +308,7 @@ export const ldapConfigServiceFactory = ({
bindCredentials: ldapConfig.bindPass, bindCredentials: ldapConfig.bindPass,
searchBase: ldapConfig.searchBase, searchBase: ldapConfig.searchBase,
searchFilter: "(uid={{username}})", searchFilter: "(uid={{username}})",
searchAttributes: ["uid", "givenName", "sn"], searchAttributes: ["uid", "uidNumber", "givenName", "sn", "mail"],
...(ldapConfig.caCert !== "" ...(ldapConfig.caCert !== ""
? { ? {
tlsOptions: { 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(); const appCfg = getConfig();
let user = await userDAL.findOne({ let userAlias = await userAliasDAL.findOne({
username, externalId,
orgId orgId,
aliasType: AuthMethod.LDAP
}); });
const organization = await orgDAL.findOrgById(orgId); const organization = await orgDAL.findOrgById(orgId);
if (!organization) throw new BadRequestError({ message: "Org not found" }); if (!organization) throw new BadRequestError({ message: "Org not found" });
if (user) { if (userAlias) {
await userDAL.transaction(async (tx) => { 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) { if (!orgMembership) {
await orgDAL.createMembership( await orgDAL.createMembership(
{ {
userId: user.id, userId: userAlias.userId,
orgId, orgId,
role: OrgMembershipRole.Member, role: OrgMembershipRole.Member,
status: OrgMembershipStatus.Accepted status: OrgMembershipStatus.Accepted
@@ -362,11 +370,12 @@ export const ldapConfigServiceFactory = ({
} }
}); });
} else { } else {
user = await userDAL.transaction(async (tx) => { userAlias = await userDAL.transaction(async (tx) => {
const uniqueUsername = await normalizeUsername(username, userDAL);
const newUser = await userDAL.create( const newUser = await userDAL.create(
{ {
username, username: uniqueUsername,
orgId, email: emails[0],
firstName, firstName,
lastName, lastName,
authMethods: [AuthMethod.LDAP], authMethods: [AuthMethod.LDAP],
@@ -374,6 +383,18 @@ export const ldapConfigServiceFactory = ({
}, },
tx tx
); );
const newUserAlias = await userAliasDAL.create(
{
userId: newUser.id,
username,
aliasType: AuthMethod.LDAP,
externalId,
emails,
orgId
},
tx
);
await orgDAL.createMembership( await orgDAL.createMembership(
{ {
userId: newUser.id, userId: newUser.id,
@@ -384,10 +405,13 @@ export const ldapConfigServiceFactory = ({
tx tx
); );
return newUser; return newUserAlias;
}); });
} }
// query for user here
const user = await userDAL.findOne({ id: userAlias.userId });
const isUserCompleted = Boolean(user.isAccepted); const isUserCompleted = Boolean(user.isAccepted);
const providerAuthToken = jwt.sign( const providerAuthToken = jwt.sign(

View File

@@ -20,9 +20,11 @@ export type TUpdateLdapCfgDTO = Partial<{
TOrgPermission; TOrgPermission;
export type TLdapLoginDTO = { export type TLdapLoginDTO = {
externalId: string;
username: string; username: string;
firstName: string; firstName: string;
lastName: string; lastName: string;
emails: string[];
orgId: string; orgId: string;
relayState?: string; relayState?: string;
}; };

View File

@@ -19,7 +19,7 @@ export const getDefaultOnPremFeatures = () => {
auditLogsRetentionDays: 0, auditLogsRetentionDays: 0,
samlSSO: false, samlSSO: false,
scim: false, scim: false,
ldap: false, ldap: true,
status: null, status: null,
trial_end: null, trial_end: null,
has_used_trial: true, has_used_trial: true,

View File

@@ -25,7 +25,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
auditLogsRetentionDays: 0, auditLogsRetentionDays: 0,
samlSSO: false, samlSSO: false,
scim: false, scim: false,
ldap: false, ldap: true,
status: null, status: null,
trial_end: null, trial_end: null,
has_used_trial: true, has_used_trial: true,

View File

@@ -26,7 +26,7 @@ export type TFeatureSet = {
auditLogsRetentionDays: 0; auditLogsRetentionDays: 0;
samlSSO: false; samlSSO: false;
scim: false; scim: false;
ldap: false; ldap: true;
status: null; status: null;
trial_end: null; trial_end: null;
has_used_trial: true; has_used_trial: true;

View File

@@ -64,7 +64,7 @@ export const secretScanningQueueFactory = ({
orgId: organizationId, orgId: organizationId,
role: OrgMembershipRole.Admin role: OrgMembershipRole.Admin
}); });
return adminsOfWork.map((userObject) => userObject.email); return adminsOfWork.filter((userObject) => userObject.email).map((userObject) => userObject.email as string);
}; };
queueService.start(QueueName.SecretPushEventScan, async (job) => { queueService.start(QueueName.SecretPushEventScan, async (job) => {
@@ -149,7 +149,7 @@ export const secretScanningQueueFactory = ({
await smtpService.sendMail({ await smtpService.sendMail({
template: SmtpTemplates.SecretLeakIncident, template: SmtpTemplates.SecretLeakIncident,
subjectLine: `Incident alert: leaked secrets found in Github repository ${repository.fullName}`, subjectLine: `Incident alert: leaked secrets found in Github repository ${repository.fullName}`,
recipients: adminEmails.filter((email) => email).map((email) => email as string), recipients: adminEmails.filter((email) => email).map((email) => email),
substitutions: { substitutions: {
numberOfSecrets: Object.keys(allFindingsByFingerprint).length, numberOfSecrets: Object.keys(allFindingsByFingerprint).length,
pusher_email: pusher.email, pusher_email: pusher.email,
@@ -221,7 +221,7 @@ export const secretScanningQueueFactory = ({
await smtpService.sendMail({ await smtpService.sendMail({
template: SmtpTemplates.SecretLeakIncident, template: SmtpTemplates.SecretLeakIncident,
subjectLine: `Incident alert: leaked secrets found in Github repository ${repository.fullName}`, subjectLine: `Incident alert: leaked secrets found in Github repository ${repository.fullName}`,
recipients: adminEmails.filter((email) => email).map((email) => email as string), recipients: adminEmails.filter((email) => email).map((email) => email),
substitutions: { substitutions: {
numberOfSecrets: findings.length numberOfSecrets: findings.length
} }

View File

@@ -104,6 +104,7 @@ import { telemetryQueueServiceFactory } from "@app/services/telemetry/telemetry-
import { telemetryServiceFactory } from "@app/services/telemetry/telemetry-service"; import { telemetryServiceFactory } from "@app/services/telemetry/telemetry-service";
import { userDALFactory } from "@app/services/user/user-dal"; import { userDALFactory } from "@app/services/user/user-dal";
import { userServiceFactory } from "@app/services/user/user-service"; import { userServiceFactory } from "@app/services/user/user-service";
import { userAliasDALFactory } from "@app/services/user-alias/user-alias-dal";
import { webhookDALFactory } from "@app/services/webhook/webhook-dal"; import { webhookDALFactory } from "@app/services/webhook/webhook-dal";
import { webhookServiceFactory } from "@app/services/webhook/webhook-service"; import { webhookServiceFactory } from "@app/services/webhook/webhook-service";
@@ -128,6 +129,7 @@ export const registerRoutes = async (
// db layers // db layers
const userDAL = userDALFactory(db); const userDAL = userDALFactory(db);
const userAliasDAL = userAliasDALFactory(db);
const authDAL = authDALFactory(db); const authDAL = authDALFactory(db);
const authTokenDAL = tokenDALFactory(db); const authTokenDAL = tokenDALFactory(db);
const orgDAL = orgDALFactory(db); const orgDAL = orgDALFactory(db);
@@ -243,6 +245,7 @@ export const registerRoutes = async (
orgDAL, orgDAL,
orgBotDAL, orgBotDAL,
userDAL, userDAL,
userAliasDAL,
permissionService, permissionService,
licenseService licenseService
}); });

View File

@@ -92,7 +92,7 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
await server.services.telemetry.sendPostHogEvents({ await server.services.telemetry.sendPostHogEvents({
event: PostHogEventTypes.AdminInit, event: PostHogEventTypes.AdminInit,
distinctId: user.user.email ?? user.user.username ?? "", distinctId: user.user.username ?? "",
properties: { properties: {
email: user.user.email ?? "", email: user.user.email ?? "",
lastName: user.user.lastName || "", lastName: user.user.lastName || "",

View File

@@ -15,7 +15,7 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
}), }),
body: z.object({ body: z.object({
emails: z.string().email().array().default([]).describe("Emails of the users to add to the project."), emails: z.string().email().array().default([]).describe("Emails of the users to add to the project."),
usernames: z.string().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: { response: {
200: z.object({ 200: z.object({
@@ -59,7 +59,8 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
}), }),
body: z.object({ body: z.object({
emails: z.string().email().array().describe("Emails of the users to remove from the project.") emails: z.string().email().array().default([]).describe("Emails of the users to remove from the project."),
usernames: z.string().array().default([]).describe("Usernames of the users to remove from the project.")
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -74,7 +75,8 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
actor: req.permission.type, actor: req.permission.type,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
projectId: req.params.projectId, projectId: req.params.projectId,
emails: req.body.emails emails: req.body.emails,
usernames: req.body.usernames
}); });
for (const membership of memberships) { for (const membership of memberships) {

View File

@@ -13,7 +13,6 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
schema: { schema: {
body: z.object({ body: z.object({
email: z.string().trim(), email: z.string().trim(),
orgId: z.string().optional(),
providerAuthToken: z.string().trim().optional(), providerAuthToken: z.string().trim().optional(),
clientPublicKey: z.string().trim() clientPublicKey: z.string().trim()
}), }),
@@ -27,7 +26,6 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
handler: async (req) => { handler: async (req) => {
const { serverPublicKey, salt } = await server.services.login.loginGenServerPublicKey({ const { serverPublicKey, salt } = await server.services.login.loginGenServerPublicKey({
email: req.body.email, email: req.body.email,
userOrgId: req.body.orgId,
clientPublicKey: req.body.clientPublicKey, clientPublicKey: req.body.clientPublicKey,
providerAuthToken: req.body.providerAuthToken providerAuthToken: req.body.providerAuthToken
}); });
@@ -45,7 +43,6 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
schema: { schema: {
body: z.object({ body: z.object({
email: z.string().trim(), email: z.string().trim(),
orgId: z.string().optional(),
providerAuthToken: z.string().trim().optional(), providerAuthToken: z.string().trim().optional(),
clientProof: z.string().trim() clientProof: z.string().trim()
}), }),
@@ -74,7 +71,6 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
const data = await server.services.login.loginExchangeClientProof({ const data = await server.services.login.loginExchangeClientProof({
email: req.body.email, email: req.body.email,
userOrgId: req.body.orgId,
ip: req.realIp, ip: req.realIp,
userAgent, userAgent,
providerAuthToken: req.body.providerAuthToken, providerAuthToken: req.body.providerAuthToken,

View File

@@ -137,7 +137,7 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => {
void server.services.telemetry.sendPostHogEvents({ void server.services.telemetry.sendPostHogEvents({
event: PostHogEventTypes.UserSignedUp, event: PostHogEventTypes.UserSignedUp,
distinctId: user.email ?? user.username ?? "", distinctId: user.username ?? "",
properties: { properties: {
email: user.email ?? "", email: user.email ?? "",
attributionSource: req.body.attributionSource attributionSource: req.body.attributionSource
@@ -202,7 +202,7 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => {
void server.services.telemetry.sendPostHogEvents({ void server.services.telemetry.sendPostHogEvents({
event: PostHogEventTypes.UserSignedUp, event: PostHogEventTypes.UserSignedUp,
distinctId: user.email ?? user.username ?? "", distinctId: user.username ?? "",
properties: { properties: {
email: user.email ?? "", email: user.email ?? "",
attributionSource: "Team Invite" attributionSource: "Team Invite"

View File

@@ -130,13 +130,11 @@ export const authLoginServiceFactory = ({ userDAL, tokenService, smtpService }:
*/ */
const loginGenServerPublicKey = async ({ const loginGenServerPublicKey = async ({
email, email,
userOrgId,
providerAuthToken, providerAuthToken,
clientPublicKey clientPublicKey
}: TLoginGenServerPublicKeyDTO) => { }: TLoginGenServerPublicKeyDTO) => {
const userEnc = await userDAL.findUserEncKeyByUsername({ const userEnc = await userDAL.findUserEncKeyByUsername({
username: email, username: email
orgId: userOrgId
}); });
if (!userEnc || (userEnc && !userEnc.isAccepted)) { if (!userEnc || (userEnc && !userEnc.isAccepted)) {
throw new Error("Failed to find user"); throw new Error("Failed to find user");
@@ -159,15 +157,13 @@ export const authLoginServiceFactory = ({ userDAL, tokenService, smtpService }:
*/ */
const loginExchangeClientProof = async ({ const loginExchangeClientProof = async ({
email, email,
userOrgId,
clientProof, clientProof,
providerAuthToken, providerAuthToken,
ip, ip,
userAgent userAgent
}: TLoginClientProofDTO) => { }: TLoginClientProofDTO) => {
const userEnc = await userDAL.findUserEncKeyByUsername({ const userEnc = await userDAL.findUserEncKeyByUsername({
username: email, username: email
orgId: userOrgId
}); });
if (!userEnc) throw new Error("Failed to find user"); if (!userEnc) throw new Error("Failed to find user");
const cfg = getConfig(); const cfg = getConfig();

View File

@@ -2,14 +2,12 @@ import { AuthMethod } from "./auth-type";
export type TLoginGenServerPublicKeyDTO = { export type TLoginGenServerPublicKeyDTO = {
email: string; email: string;
userOrgId?: string;
clientPublicKey: string; clientPublicKey: string;
providerAuthToken?: string; providerAuthToken?: string;
}; };
export type TLoginClientProofDTO = { export type TLoginClientProofDTO = {
email: string; email: string;
userOrgId?: string;
clientProof: string; clientProof: string;
providerAuthToken?: string; providerAuthToken?: string;
ip: string; ip: string;

View File

@@ -92,7 +92,7 @@ export const orgDALFactory = (db: TDbClient) => {
const findOrgMembersByUsername = async (orgId: string, usernames: string[]) => { const findOrgMembersByUsername = async (orgId: string, usernames: string[]) => {
try { try {
const members = await db(TableName.OrgMembership) const members = await db(TableName.OrgMembership)
.where({ orgId }) .where(`${TableName.OrgMembership}.orgId`, orgId)
.join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`) .join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`)
.leftJoin<TUserEncryptionKeys>( .leftJoin<TUserEncryptionKeys>(
TableName.UserEncryptionKey, TableName.UserEncryptionKey,

View File

@@ -441,7 +441,7 @@ export const orgServiceFactory = ({
recipients: [inviteeEmail], recipients: [inviteeEmail],
substitutions: { substitutions: {
inviterFirstName: user.firstName, inviterFirstName: user.firstName,
inviterEmail: user.email, inviterUsername: user.username,
organizationName: org?.name, organizationName: org?.name,
email: inviteeEmail, email: inviteeEmail,
organizationId: org?.id.toString(), organizationId: org?.id.toString(),

View File

@@ -57,7 +57,7 @@ export const projectMembershipDALFactory = (db: TDbClient) => {
} }
}; };
const findMembershipsByEmail = async (projectId: string, emails: string[]) => { const findMembershipsByUsername = async (projectId: string, usernames: string[]) => {
try { try {
const members = await db(TableName.ProjectMembership) const members = await db(TableName.ProjectMembership)
.where({ projectId }) .where({ projectId })
@@ -70,18 +70,18 @@ export const projectMembershipDALFactory = (db: TDbClient) => {
.select( .select(
selectAllTableCols(TableName.ProjectMembership), selectAllTableCols(TableName.ProjectMembership),
db.ref("id").withSchema(TableName.Users).as("userId"), db.ref("id").withSchema(TableName.Users).as("userId"),
db.ref("email").withSchema(TableName.Users) db.ref("username").withSchema(TableName.Users)
) )
.whereIn("email", emails) .whereIn("username", usernames)
.where({ isGhost: false }); .where({ isGhost: false });
return members.map(({ userId, email, ...data }) => ({ return members.map(({ userId, username, ...data }) => ({
...data, ...data,
user: { id: userId, email } user: { id: userId, username }
})); }));
} catch (error) { } catch (error) {
throw new DatabaseError({ error, name: "Find members by email" }); throw new DatabaseError({ error, name: "Find members by email" });
} }
}; };
return { ...projectMemberOrm, findAllProjectMembers, findProjectGhostUser, findMembershipsByEmail }; return { ...projectMemberOrm, findAllProjectMembers, findProjectGhostUser, findMembershipsByUsername };
}; };

View File

@@ -134,7 +134,7 @@ export const projectMembershipServiceFactory = ({
const appCfg = getConfig(); const appCfg = getConfig();
await smtpService.sendMail({ await smtpService.sendMail({
template: SmtpTemplates.WorkspaceInvite, template: SmtpTemplates.WorkspaceInvite,
subjectLine: "Infisical workspace invitation", subjectLine: "Infisical project invitation",
recipients: invitees.filter((i) => i.email).map((i) => i.email as string), recipients: invitees.filter((i) => i.email).map((i) => i.email as string),
substitutions: { substitutions: {
workspaceName: project.name, workspaceName: project.name,
@@ -206,10 +206,8 @@ export const projectMembershipServiceFactory = ({
const appCfg = getConfig(); const appCfg = getConfig();
await smtpService.sendMail({ await smtpService.sendMail({
template: SmtpTemplates.WorkspaceInvite, template: SmtpTemplates.WorkspaceInvite,
subjectLine: "Infisical workspace invitation", subjectLine: "Infisical project invitation",
recipients: orgMembers recipients: orgMembers.filter((i) => i.email).map((i) => i.email as string),
.map(({ email }) => email)
.filter((email): email is string => email !== null && email !== undefined),
substitutions: { substitutions: {
workspaceName: project.name, workspaceName: project.name,
callback_url: `${appCfg.SITE_URL}/login` callback_url: `${appCfg.SITE_URL}/login`
@@ -237,11 +235,14 @@ export const projectMembershipServiceFactory = ({
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId); const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Member); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Member);
const usernamesAndEmails = [...emails, ...usernames];
const orgMembers = await orgDAL.findOrgMembersByUsername(project.orgId, [ 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 []; if (!orgMembers.length) return [];
@@ -320,16 +321,21 @@ export const projectMembershipServiceFactory = ({
}); });
if (sendEmails) { if (sendEmails) {
const recipients = orgMembers.filter((i) => i.user.email).map((i) => i.user.email as string);
const appCfg = getConfig(); const appCfg = getConfig();
await smtpService.sendMail({
template: SmtpTemplates.WorkspaceInvite, if (recipients.length) {
subjectLine: "Infisical workspace invitation", await smtpService.sendMail({
recipients: orgMembers.filter(({ user }) => user.email).map(({ user }) => user.email as string), template: SmtpTemplates.WorkspaceInvite,
substitutions: { subjectLine: "Infisical project invitation",
workspaceName: project.name, recipients: orgMembers.filter((i) => i.user.email).map((i) => i.user.email as string),
callback_url: `${appCfg.SITE_URL}/login` substitutions: {
} workspaceName: project.name,
}); callback_url: `${appCfg.SITE_URL}/login`
}
});
}
} }
return members; return members;
}; };
@@ -412,7 +418,8 @@ export const projectMembershipServiceFactory = ({
actor, actor,
actorOrgId, actorOrgId,
projectId, projectId,
emails emails,
usernames
}: TDeleteProjectMembershipsDTO) => { }: TDeleteProjectMembershipsDTO) => {
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Member); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Member);
@@ -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({ throw new BadRequestError({
message: "Some users are not part of project", message: "Some users are not part of project",
name: "Delete project membership" name: "Delete project membership"

View File

@@ -18,6 +18,7 @@ export type TDeleteProjectMembershipOldDTO = {
export type TDeleteProjectMembershipsDTO = { export type TDeleteProjectMembershipsDTO = {
emails: string[]; emails: string[];
usernames: string[];
} & TProjectPermission; } & TProjectPermission;
export type TAddUsersToWorkspaceDTO = { export type TAddUsersToWorkspaceDTO = {

View File

@@ -8,7 +8,7 @@
</head> </head>
<body> <body>
<h2>Join your organization on Infisical</h2> <h2>Join your organization on Infisical</h2>
<p>{{inviterFirstName}} ({{inviterEmail}}) has invited you to their Infisical organization — {{organizationName}}</p> <p>{{inviterFirstName}} ({{inviterUsername}}) has invited you to their Infisical organization — {{organizationName}}</p>
<a href="{{callback_url}}?token={{token}}&to={{email}}&organization_id={{organizationId}}">Join now</a> <a href="{{callback_url}}?token={{token}}&to={{email}}&organization_id={{organizationId}}">Join now</a>
<h3>What is Infisical?</h3> <h3>What is Infisical?</h3>
<p>Infisical is an easy-to-use end-to-end encrypted tool that enables developers to sync and manage their secrets and configs.</p> <p>Infisical is an easy-to-use end-to-end encrypted tool that enables developers to sync and manage their secrets and configs.</p>

View File

@@ -0,0 +1,13 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TUserAliasDALFactory = ReturnType<typeof userAliasDALFactory>;
export const userAliasDALFactory = (db: TDbClient) => {
const userAliasOrm = ormify(db, TableName.UserAliases);
return {
...userAliasOrm
};
};

View File

@@ -20,12 +20,11 @@ export const userDALFactory = (db: TDbClient) => {
// USER ENCRYPTION FUNCTIONS // USER ENCRYPTION FUNCTIONS
// ------------------------- // -------------------------
const findUserEncKeyByUsername = async ({ username, orgId }: { username: string; orgId?: string }) => { const findUserEncKeyByUsername = async ({ username }: { username: string }) => {
try { try {
return await db(TableName.Users) return await db(TableName.Users)
.where({ .where({
username, username,
...(orgId ? { orgId } : { orgId: null }),
isGhost: false isGhost: false
}) })
.join(TableName.UserEncryptionKey, `${TableName.Users}.id`, `${TableName.UserEncryptionKey}.userId`) .join(TableName.UserEncryptionKey, `${TableName.Users}.id`, `${TableName.UserEncryptionKey}.userId`)

View File

@@ -0,0 +1,21 @@
import slugify from "@sindresorhus/slugify";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { TUserDALFactory } from "@app/services/user/user-dal";
export const normalizeUsername = async (username: string, userDAL: Pick<TUserDALFactory, "findOne">) => {
let attempt = slugify(username);
let user = await userDAL.findOne({ username: attempt });
if (!user) return attempt;
while (true) {
attempt = slugify(`${username}-${alphaNumericNanoId(4)}`);
// eslint-disable-next-line no-await-in-loop
user = await userDAL.findOne({ username: attempt });
if (!user) {
return attempt;
}
}
};

View File

@@ -21,12 +21,10 @@ interface IsLoginSuccessful {
*/ */
const attemptLogin = async ({ const attemptLogin = async ({
email, email,
orgId,
password, password,
providerAuthToken providerAuthToken
}: { }: {
email: string; email: string;
orgId?: string;
password: string; password: string;
providerAuthToken?: string; providerAuthToken?: string;
}): Promise<IsLoginSuccessful> => { }): Promise<IsLoginSuccessful> => {
@@ -40,7 +38,6 @@ const attemptLogin = async ({
const { serverPublicKey, salt } = await login1({ const { serverPublicKey, salt } = await login1({
email, email,
orgId,
clientPublicKey, clientPublicKey,
providerAuthToken providerAuthToken
}); });
@@ -62,7 +59,6 @@ const attemptLogin = async ({
tag tag
} = await login2({ } = await login2({
email, email,
orgId,
clientProof, clientProof,
providerAuthToken providerAuthToken
}); });

View File

@@ -25,14 +25,12 @@ export type VerifyMfaTokenRes = {
export type Login1DTO = { export type Login1DTO = {
email: string; email: string;
orgId?: string;
clientPublicKey: string; clientPublicKey: string;
providerAuthToken?: string; providerAuthToken?: string;
} }
export type Login2DTO = { export type Login2DTO = {
email: string; email: string;
orgId?: string;
clientProof: string; clientProof: string;
providerAuthToken?: string; providerAuthToken?: string;
} }

View File

@@ -50,9 +50,9 @@ export const useAddUserToWsNonE2EE = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation<{}, {}, AddUserToWsDTONonE2EE>({ return useMutation<{}, {}, AddUserToWsDTONonE2EE>({
mutationFn: async ({ projectId, emails }) => { mutationFn: async ({ projectId, usernames }) => {
const { data } = await apiRequest.post(`/api/v2/workspace/${projectId}/memberships`, { const { data } = await apiRequest.post(`/api/v2/workspace/${projectId}/memberships`, {
emails usernames
}); });
return data; return data;
}, },

View File

@@ -78,7 +78,7 @@ export type AddUserToWsDTOE2EE = {
export type AddUserToWsDTONonE2EE = { export type AddUserToWsDTONonE2EE = {
projectId: string; projectId: string;
emails: string[]; usernames: string[];
}; };
export type UpdateOrgUserRoleDTO = { export type UpdateOrgUserRoleDTO = {

View File

@@ -323,11 +323,11 @@ export const useDeleteUserFromWorkspace = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: async ({ emails, workspaceId }: { workspaceId: string; emails: string[] }) => { mutationFn: async ({ usernames, workspaceId }: { workspaceId: string; usernames: string[] }) => {
const { const {
data: { deletedMembership } data: { deletedMembership }
} = await apiRequest.delete(`/api/v2/workspace/${workspaceId}/memberships`, { } = await apiRequest.delete(`/api/v2/workspace/${workspaceId}/memberships`, {
data: { emails } data: { usernames }
}); });
return deletedMembership; return deletedMembership;
}, },

View File

@@ -78,7 +78,6 @@ export const PasswordStep = ({
} else { } else {
const loginAttempt = await attemptLogin({ const loginAttempt = await attemptLogin({
email, email,
orgId: organizationId,
password, password,
providerAuthToken, providerAuthToken,
}); });

View File

@@ -111,7 +111,7 @@ export const MemberListTab = () => {
const orgUser = (orgUsers || []).find(({ id }) => id === orgMembershipId); const orgUser = (orgUsers || []).find(({ id }) => id === orgMembershipId);
if (!orgUser) return; if (!orgUser) return;
try { try { // TODO: update
if (currentWorkspace.version === ProjectVersion.V1) { if (currentWorkspace.version === ProjectVersion.V1) {
await addUserToWorkspace({ await addUserToWorkspace({
workspaceId, workspaceId,
@@ -122,7 +122,7 @@ export const MemberListTab = () => {
} else if (currentWorkspace.version === ProjectVersion.V2) { } else if (currentWorkspace.version === ProjectVersion.V2) {
await addUserToWorkspaceNonE2EE({ await addUserToWorkspaceNonE2EE({
projectId: workspaceId, projectId: workspaceId,
emails: [orgUser.user.username] usernames: [orgUser.user.username]
}); });
} else { } else {
createNotification({ createNotification({
@@ -148,11 +148,11 @@ export const MemberListTab = () => {
}; };
const handleRemoveUser = async () => { const handleRemoveUser = async () => {
const email = (popUp?.removeMember?.data as { email: string })?.email; const username = (popUp?.removeMember?.data as { username: string })?.username;
if (!currentOrg?.id) return; if (!currentOrg?.id) return;
try { try {
await removeUserFromWorkspace({ workspaceId, emails: [email] }); await removeUserFromWorkspace({ workspaceId, usernames: [username] });
createNotification({ createNotification({
text: "Successfully removed user from project", text: "Successfully removed user from project",
type: "success" type: "success"
@@ -222,12 +222,12 @@ export const MemberListTab = () => {
); );
const filteredOrgUsers = useMemo(() => { const filteredOrgUsers = useMemo(() => {
const wsUserEmails = new Map(); const wsUserUsernames = new Map();
members?.forEach((member) => { members?.forEach((member) => {
wsUserEmails.set(member.user.email, true); wsUserUsernames.set(member.user.username, true);
}); });
return (orgUsers || []).filter( return (orgUsers || []).filter(
({ status, user: u }) => status === "accepted" && !wsUserEmails.has(u.email) ({ status, user: u }) => status === "accepted" && !wsUserUsernames.has(u.username)
); );
}, [orgUsers, members]); }, [orgUsers, members]);
@@ -322,7 +322,7 @@ export const MemberListTab = () => {
className="ml-4" className="ml-4"
isDisabled={userId === u?.id || !isAllowed} isDisabled={userId === u?.id || !isAllowed}
onClick={() => onClick={() =>
handlePopUpOpen("removeMember", { email: u.email }) handlePopUpOpen("removeMember", { username: u.username })
} }
> >
<FontAwesomeIcon icon={faXmark} /> <FontAwesomeIcon icon={faXmark} />
@@ -354,20 +354,20 @@ export const MemberListTab = () => {
<form onSubmit={handleSubmit(onAddMember)}> <form onSubmit={handleSubmit(onAddMember)}>
<Controller <Controller
control={control} control={control}
defaultValue={filteredOrgUsers?.[0]?.user?.email} defaultValue={filteredOrgUsers?.[0]?.user?.username}
name="orgMembershipId" name="orgMembershipId"
render={({ field, fieldState: { error } }) => ( render={({ field, fieldState: { error } }) => (
<FormControl label="Email" isError={Boolean(error)} errorText={error?.message}> <FormControl label="Username" isError={Boolean(error)} errorText={error?.message}>
<Select <Select
position="popper" position="popper"
className="w-full" className="w-full"
defaultValue={filteredOrgUsers?.[0]?.user?.email} defaultValue={filteredOrgUsers?.[0]?.user?.username}
value={field.value} value={field.value}
onValueChange={field.onChange} onValueChange={field.onChange}
> >
{filteredOrgUsers.map(({ id: orgUserId, user: u }) => ( {filteredOrgUsers.map(({ id: orgUserId, user: u }) => (
<SelectItem value={orgUserId} key={`org-membership-join-${orgUserId}`}> <SelectItem value={orgUserId} key={`org-membership-join-${orgUserId}`}>
{u?.email} {u?.username}
</SelectItem> </SelectItem>
))} ))}
</Select> </Select>