Skip to content

Commit

Permalink
Add user aliases concept and weave LDAP into it
Browse files Browse the repository at this point in the history
  • Loading branch information
dangtony98 committed Mar 6, 2024
1 parent 327c5e2 commit 76bd85e
Show file tree
Hide file tree
Showing 39 changed files with 206 additions and 102 deletions.
4 changes: 4 additions & 0 deletions backend/src/@types/knex.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,9 @@ import {
TUserActions,
TUserActionsInsert,
TUserActionsUpdate,
TUserAliases,
TUserAliasesInsert,
TUserAliasesUpdate,
TUserEncryptionKeys,
TUserEncryptionKeysInsert,
TUserEncryptionKeysUpdate,
Expand All @@ -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,
Expand Down
28 changes: 21 additions & 7 deletions backend/src/db/migrations/20240305165532_ldap-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions backend/src/db/schemas/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
4 changes: 2 additions & 2 deletions backend/src/db/schemas/ldap-configs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>>;
1 change: 1 addition & 0 deletions backend/src/db/schemas/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
24 changes: 24 additions & 0 deletions backend/src/db/schemas/user-aliases.ts
Original file line number Diff line number Diff line change
@@ -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>>;
3 changes: 1 addition & 2 deletions backend/src/db/schemas/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>;
Expand Down
2 changes: 2 additions & 0 deletions backend/src/ee/routes/v1/ldap-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
});
Expand Down
2 changes: 1 addition & 1 deletion backend/src/ee/routes/v1/scim-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
48 changes: 36 additions & 12 deletions backend/src/ee/services/ldap-config/ldap-config-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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">;
};
Expand All @@ -45,6 +48,7 @@ export const ldapConfigServiceFactory = ({
orgDAL,
orgBotDAL,
userDAL,
userAliasDAL,
permissionService,
licenseService
}: TLdapConfigServiceFactoryDep) => {
Expand Down Expand Up @@ -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
Expand All @@ -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: {
Expand All @@ -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
Expand All @@ -362,18 +370,31 @@ 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],
isGhost: false
},
tx
);
const newUserAlias = await userAliasDAL.create(
{
userId: newUser.id,
username,
aliasType: AuthMethod.LDAP,
externalId,
emails,
orgId
},
tx
);

await orgDAL.createMembership(
{
userId: newUser.id,
Expand All @@ -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(
Expand Down
2 changes: 2 additions & 0 deletions backend/src/ee/services/ldap-config/ldap-config-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
2 changes: 1 addition & 1 deletion backend/src/ee/services/license/__mocks__/licence-fns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion backend/src/ee/services/license/licence-fns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion backend/src/ee/services/license/license-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
}
Expand Down
3 changes: 3 additions & 0 deletions backend/src/server/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -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);
Expand Down Expand Up @@ -243,6 +245,7 @@ export const registerRoutes = async (
orgDAL,
orgBotDAL,
userDAL,
userAliasDAL,
permissionService,
licenseService
});
Expand Down
2 changes: 1 addition & 1 deletion backend/src/server/routes/v1/admin-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 || "",
Expand Down
8 changes: 5 additions & 3 deletions backend/src/server/routes/v2/project-membership-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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({
Expand All @@ -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) {
Expand Down
Loading

0 comments on commit 76bd85e

Please sign in to comment.