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,
TUserActionsInsert,
TUserActionsUpdate,
TUserAliases,
TUserAliasesInsert,
TUserAliasesUpdate,
TUserEncryptionKeys,
TUserEncryptionKeysInsert,
TUserEncryptionKeysUpdate,
@ -178,6 +181,7 @@ import {
declare module "knex/types/tables" {
interface Tables {
[TableName.Users]: Knex.CompositeTableType<TUsers, TUsersInsert, TUsersUpdate>;
[TableName.UserAliases]: Knex.CompositeTableType<TUserAliases, TUserAliasesInsert, TUserAliasesUpdate>;
[TableName.UserEncryptionKey]: Knex.CompositeTableType<
TUserEncryptionKeys,
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) => {
t.string("username").notNullable();
t.uuid("orgId").nullable();
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
t.string("username").unique().notNullable();
t.string("email").nullable().alter();
t.unique(["username", "orgId"]);
});
await knex(TableName.Users).update("username", knex.ref("email"));
await createOnUpdateTrigger(knex, TableName.LdapConfig);
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists(TableName.LdapConfig);
await knex.schema.dropTableIfExists(TableName.UserAliases);
await knex.schema.alterTable(TableName.Users, (t) => {
t.dropColumn("username");
t.dropColumn("orgId");
// t.string("email").notNullable().alter();
});
await dropOnUpdateTrigger(knex, TableName.LdapConfig);

View File

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

View File

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

View File

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

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(),
updatedAt: z.date(),
isGhost: z.boolean().default(false),
username: z.string(),
orgId: z.string().uuid().nullable().optional()
username: z.string()
});
export type TUsers = z.infer<typeof UsersSchema>;

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

@ -8,7 +8,7 @@
</head>
<body>
<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>
<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>

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
// -------------------------
const findUserEncKeyByUsername = async ({ username, orgId }: { username: string; orgId?: string }) => {
const findUserEncKeyByUsername = async ({ username }: { username: string }) => {
try {
return await db(TableName.Users)
.where({
username,
...(orgId ? { orgId } : { orgId: null }),
isGhost: false
})
.join(TableName.UserEncryptionKey, `${TableName.Users}.id`, `${TableName.UserEncryptionKey}.userId`)

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 ({
email,
orgId,
password,
providerAuthToken
}: {
email: string;
orgId?: string;
password: string;
providerAuthToken?: string;
}): Promise<IsLoginSuccessful> => {
@ -40,7 +38,6 @@ const attemptLogin = async ({
const { serverPublicKey, salt } = await login1({
email,
orgId,
clientPublicKey,
providerAuthToken
});
@ -62,7 +59,6 @@ const attemptLogin = async ({
tag
} = await login2({
email,
orgId,
clientProof,
providerAuthToken
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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