mirror of
https://github.com/Infisical/infisical.git
synced 2025-03-28 15:29:21 +00:00
Add user aliases concept and weave LDAP into it
This commit is contained in:
4
backend/src/@types/knex.d.ts
vendored
4
backend/src/@types/knex.d.ts
vendored
@ -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,
|
||||
|
@ -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);
|
||||
|
@ -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";
|
||||
|
@ -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>>;
|
||||
|
@ -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",
|
||||
|
24
backend/src/db/schemas/user-aliases.ts
Normal file
24
backend/src/db/schemas/user-aliases.ts
Normal 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>>;
|
@ -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>;
|
||||
|
@ -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
|
||||
});
|
||||
|
@ -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
|
||||
|
@ -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(
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
});
|
||||
|
@ -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 || "",
|
||||
|
@ -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) {
|
||||
|
@ -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,
|
||||
|
@ -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"
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
|
@ -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(),
|
||||
|
@ -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 };
|
||||
};
|
||||
|
@ -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"
|
||||
|
@ -18,6 +18,7 @@ export type TDeleteProjectMembershipOldDTO = {
|
||||
|
||||
export type TDeleteProjectMembershipsDTO = {
|
||||
emails: string[];
|
||||
usernames: string[];
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TAddUsersToWorkspaceDTO = {
|
||||
|
@ -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>
|
||||
|
13
backend/src/services/user-alias/user-alias-dal.ts
Normal file
13
backend/src/services/user-alias/user-alias-dal.ts
Normal 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
|
||||
};
|
||||
};
|
0
backend/src/services/user-alias/user-alias-types.ts
Normal file
0
backend/src/services/user-alias/user-alias-types.ts
Normal 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`)
|
||||
|
21
backend/src/services/user/user-fns.ts
Normal file
21
backend/src/services/user/user-fns.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
};
|
@ -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
|
||||
});
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
},
|
||||
|
@ -78,7 +78,7 @@ export type AddUserToWsDTOE2EE = {
|
||||
|
||||
export type AddUserToWsDTONonE2EE = {
|
||||
projectId: string;
|
||||
emails: string[];
|
||||
usernames: string[];
|
||||
};
|
||||
|
||||
export type UpdateOrgUserRoleDTO = {
|
||||
|
@ -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;
|
||||
},
|
||||
|
@ -78,7 +78,6 @@ export const PasswordStep = ({
|
||||
} else {
|
||||
const loginAttempt = await attemptLogin({
|
||||
email,
|
||||
orgId: organizationId,
|
||||
password,
|
||||
providerAuthToken,
|
||||
});
|
||||
|
@ -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>
|
||||
|
Reference in New Issue
Block a user