Patch unsynchronized username/email for saml/scim

This commit is contained in:
Tuan Dang
2024-05-06 18:27:36 -07:00
parent 30ccb78c81
commit 3b88a2759b
24 changed files with 322 additions and 138 deletions
+3 -3
View File
@@ -1,6 +1,6 @@
import { Knex } from "knex";
import { SecretKeyEncoding, TUsers } from "@app/db/schemas";
import { SecretKeyEncoding, TableName, TUsers } from "@app/db/schemas";
import { decryptAsymmetric, encryptAsymmetric, infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
import { BadRequestError, ScimRequestError } from "@app/lib/errors";
@@ -188,9 +188,9 @@ export const addUsersToGroupByUserIds = async ({
// check if all user(s) are part of the organization
const existingUserOrgMemberships = await orgDAL.findMembership(
{
orgId: group.orgId,
[`${TableName.OrgMembership}.orgId` as "orgId"]: group.orgId,
$in: {
userId: userIds
[`${TableName.OrgMembership}.userId` as "userId"]: userIds
}
},
{ tx }
@@ -6,7 +6,8 @@ import {
OrgMembershipStatus,
SecretKeyEncoding,
TableName,
TLdapConfigsUpdate
TLdapConfigsUpdate,
TUsers
} from "@app/db/schemas";
import { TGroupDALFactory } from "@app/ee/services/group/group-dal";
import { addUsersToGroupByUserIds, removeUsersFromGroupByUserIds } from "@app/ee/services/group/group-fns";
@@ -25,6 +26,7 @@ import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type";
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal";
import { TOrgDALFactory } from "@app/services/org/org-dal";
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal";
@@ -54,6 +56,7 @@ import { TLdapGroupMapDALFactory } from "./ldap-group-map-dal";
type TLdapConfigServiceFactoryDep = {
ldapConfigDAL: Pick<TLdapConfigDALFactory, "create" | "update" | "findOne">;
ldapGroupMapDAL: Pick<TLdapGroupMapDALFactory, "find" | "create" | "delete" | "findLdapGroupMapsByLdapConfigId">;
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "create">;
orgDAL: Pick<
TOrgDALFactory,
"createMembership" | "updateMembershipById" | "findMembership" | "findOrgById" | "findOne" | "updateById"
@@ -83,6 +86,7 @@ export const ldapConfigServiceFactory = ({
ldapConfigDAL,
ldapGroupMapDAL,
orgDAL,
orgMembershipDAL,
orgBotDAL,
groupDAL,
groupProjectDAL,
@@ -387,7 +391,7 @@ export const ldapConfigServiceFactory = ({
username,
firstName,
lastName,
emails,
email,
groups,
orgId,
relayState
@@ -407,7 +411,7 @@ export const ldapConfigServiceFactory = ({
await userDAL.transaction(async (tx) => {
const [orgMembership] = await orgDAL.findMembership(
{
userId: userAlias.userId,
[`${TableName.OrgMembership}.userId` as "userId"]: userAlias.userId,
[`${TableName.OrgMembership}.orgId` as "id"]: orgId
},
{ tx }
@@ -434,41 +438,75 @@ export const ldapConfigServiceFactory = ({
});
} else {
userAlias = await userDAL.transaction(async (tx) => {
const uniqueUsername = await normalizeUsername(username, userDAL);
const newUser = await userDAL.create(
{
username: uniqueUsername,
email: emails[0],
isEmailVerified: serverCfg.trustLdapEmails,
firstName,
lastName,
authMethods: [],
isGhost: false
},
tx
);
let newUser: TUsers | undefined;
if (serverCfg.trustSamlEmails) {
newUser = await userDAL.findOne(
{
email,
isEmailVerified: true
},
tx
);
}
if (!newUser) {
const uniqueUsername = await normalizeUsername(username, userDAL);
newUser = await userDAL.create(
{
username: serverCfg.trustLdapEmails ? email : uniqueUsername,
email,
isEmailVerified: serverCfg.trustLdapEmails,
firstName,
lastName,
authMethods: [],
isGhost: false
},
tx
);
}
const newUserAlias = await userAliasDAL.create(
{
userId: newUser.id,
username,
aliasType: UserAliasType.LDAP,
externalId,
emails,
emails: [email],
orgId
},
tx
);
await orgDAL.createMembership(
const [orgMembership] = await orgDAL.findMembership(
{
userId: newUser.id,
orgId,
role: OrgMembershipRole.Member,
status: OrgMembershipStatus.Invited
[`${TableName.OrgMembership}.userId` as "userId"]: newUser.id,
[`${TableName.OrgMembership}.orgId` as "id"]: orgId
},
tx
{ tx }
);
if (!orgMembership) {
await orgMembershipDAL.create(
{
userId: userAlias.userId,
inviteEmail: email,
orgId,
role: OrgMembershipRole.Member,
status: newUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later
},
tx
);
// Only update the membership to Accepted if the user account is already completed.
} else if (orgMembership.status === OrgMembershipStatus.Invited && newUser.isAccepted) {
await orgDAL.updateMembershipById(
orgMembership.id,
{
status: OrgMembershipStatus.Accepted
},
tx
);
}
return newUserAlias;
});
}
@@ -51,7 +51,7 @@ export type TLdapLoginDTO = {
username: string;
firstName: string;
lastName: string;
emails: string[];
email: string;
orgId: string;
groups?: {
dn: string;
@@ -347,7 +347,7 @@ export const samlConfigServiceFactory = ({
const foundUser = await userDAL.findById(userAlias.userId, tx);
const [orgMembership] = await orgDAL.findMembership(
{
userId: foundUser.id,
[`${TableName.OrgMembership}.userId` as "userId"]: foundUser.id,
[`${TableName.OrgMembership}.orgId` as "id"]: orgId
},
{ tx }
@@ -378,19 +378,33 @@ export const samlConfigServiceFactory = ({
});
} else {
user = await userDAL.transaction(async (tx) => {
const uniqueUsername = await normalizeUsername(`${firstName ?? ""}-${lastName ?? ""}`, userDAL);
const newUser = await userDAL.create(
{
username: uniqueUsername,
email,
isEmailVerified: serverCfg.trustSamlEmails,
firstName,
lastName,
authMethods: [],
isGhost: false
},
tx
);
let newUser: TUsers | undefined;
if (serverCfg.trustSamlEmails) {
newUser = await userDAL.findOne(
{
email,
isEmailVerified: true
},
tx
);
}
if (!newUser) {
const uniqueUsername = await normalizeUsername(`${firstName ?? ""}-${lastName ?? ""}`, userDAL);
newUser = await userDAL.create(
{
username: serverCfg.trustSamlEmails ? email : uniqueUsername,
email,
isEmailVerified: serverCfg.trustSamlEmails,
firstName,
lastName,
authMethods: [],
isGhost: false
},
tx
);
}
await userAliasDAL.create(
{
userId: newUser.id,
@@ -402,17 +416,36 @@ export const samlConfigServiceFactory = ({
tx
);
await orgMembershipDAL.create(
const [orgMembership] = await orgDAL.findMembership(
{
userId: newUser.id,
inviteEmail: email,
orgId,
role: OrgMembershipRole.Member,
status: OrgMembershipStatus.Invited
[`${TableName.OrgMembership}.userId` as "userId"]: newUser.id,
[`${TableName.OrgMembership}.orgId` as "id"]: orgId
},
tx
{ tx }
);
if (!orgMembership) {
await orgMembershipDAL.create(
{
userId: newUser.id,
inviteEmail: email,
orgId,
role: OrgMembershipRole.Member,
status: newUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later
},
tx
);
// Only update the membership to Accepted if the user account is already completed.
} else if (orgMembership.status === OrgMembershipStatus.Invited && newUser.isAccepted) {
await orgDAL.updateMembershipById(
orgMembership.id,
{
status: OrgMembershipStatus.Accepted
},
tx
);
}
return newUser;
});
}
@@ -46,7 +46,7 @@ export type TGetSamlCfgDTO =
export type TSamlLoginDTO = {
externalId: string;
email?: string;
email: string;
firstName: string;
lastName?: string;
authProvider: string;
+76 -38
View File
@@ -209,10 +209,10 @@ export const scimServiceFactory = ({
findOpts
);
const scimUsers = users.map(({ id, username, firstName, lastName, email }) =>
const scimUsers = users.map(({ id, externalId, username, firstName, lastName, email }) =>
buildScimUser({
orgMembershipId: id ?? "",
username,
username: externalId ?? username,
firstName: firstName ?? "",
lastName: lastName ?? "",
email,
@@ -254,7 +254,7 @@ export const scimServiceFactory = ({
return buildScimUser({
orgMembershipId: membership.id,
username: membership.username,
username: membership.externalId ?? membership.username,
email: membership.email ?? "",
firstName: membership.firstName as string,
lastName: membership.lastName as string,
@@ -262,7 +262,9 @@ export const scimServiceFactory = ({
});
};
const createScimUser = async ({ username, email, firstName, lastName, orgId }: TCreateScimUserDTO) => {
const createScimUser = async ({ externalId, email, firstName, lastName, orgId }: TCreateScimUserDTO) => {
if (!email) throw new ScimRequestError({ detail: "Invalid request. Missing email.", status: 400 });
const org = await orgDAL.findById(orgId);
if (!org)
@@ -281,13 +283,13 @@ export const scimServiceFactory = ({
const serverCfg = await getServerCfg();
const userAlias = await userAliasDAL.findOne({
externalId: username,
externalId,
orgId,
aliasType: UserAliasType.SAML
});
const { user: createdUser, orgMembership: createdOrgMembership } = await userDAL.transaction(async (tx) => {
let user: TUsers;
let user: TUsers | undefined;
let orgMembership: TOrgMemberships;
if (userAlias) {
user = await userDAL.findById(userAlias.userId, tx);
@@ -320,39 +322,74 @@ export const scimServiceFactory = ({
);
}
} else {
const uniqueUsername = await normalizeUsername(`${firstName}-${lastName}`, userDAL);
user = await userDAL.create(
{
username: uniqueUsername,
email,
isEmailVerified: serverCfg.trustSamlEmails,
firstName,
lastName,
authMethods: [],
isGhost: false
},
tx
);
if (serverCfg.trustSamlEmails) {
user = await userDAL.findOne(
{
email,
isEmailVerified: true
},
tx
);
}
if (!user) {
const uniqueUsername = await normalizeUsername(`${firstName}-${lastName}`, userDAL);
user = await userDAL.create(
{
username: serverCfg.trustSamlEmails ? email : uniqueUsername,
email,
isEmailVerified: serverCfg.trustSamlEmails,
firstName,
lastName,
authMethods: [],
isGhost: false
},
tx
);
}
await userAliasDAL.create(
{
userId: user.id,
aliasType: UserAliasType.SAML,
externalId: username,
externalId,
emails: email ? [email] : [],
orgId
},
tx
);
orgMembership = await orgMembershipDAL.create(
const [foundOrgMembership] = await orgDAL.findMembership(
{
userId: user.id,
inviteEmail: email,
orgId,
role: OrgMembershipRole.Member,
status: OrgMembershipStatus.Invited
[`${TableName.OrgMembership}.userId` as "userId"]: user.id,
[`${TableName.OrgMembership}.orgId` as "id"]: orgId
},
tx
{ tx }
);
orgMembership = foundOrgMembership;
if (!orgMembership) {
orgMembership = await orgMembershipDAL.create(
{
userId: user.id,
inviteEmail: email,
orgId,
role: OrgMembershipRole.Member,
status: user.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later
},
tx
);
// Only update the membership to Accepted if the user account is already completed.
} else if (orgMembership.status === OrgMembershipStatus.Invited && user.isAccepted) {
orgMembership = await orgDAL.updateMembershipById(
orgMembership.id,
{
status: OrgMembershipStatus.Accepted
},
tx
);
}
}
return { user, orgMembership };
@@ -372,7 +409,7 @@ export const scimServiceFactory = ({
return buildScimUser({
orgMembershipId: createdOrgMembership.id,
username: createdUser.username,
username: externalId,
firstName: createdUser.firstName as string,
lastName: createdUser.lastName as string,
email: createdUser.email ?? "",
@@ -380,11 +417,11 @@ export const scimServiceFactory = ({
});
};
const updateScimUser = async ({ userId, orgId, operations }: TUpdateScimUserDTO) => {
const updateScimUser = async ({ orgMembershipId, orgId, operations }: TUpdateScimUserDTO) => {
const [membership] = await orgDAL
.findMembership({
userId,
[`${TableName.OrgMembership}.orgId` as "id"]: orgId
[`${TableName.OrgMembership}.id` as "id"]: orgMembershipId,
[`${TableName.OrgMembership}.orgId` as "orgId"]: orgId
})
.catch(() => {
throw new ScimRequestError({
@@ -433,7 +470,7 @@ export const scimServiceFactory = ({
return buildScimUser({
orgMembershipId: membership.id,
username: membership.username,
username: membership.externalId ?? membership.username,
email: membership.email,
firstName: membership.firstName as string,
lastName: membership.lastName as string,
@@ -467,7 +504,6 @@ export const scimServiceFactory = ({
});
if (!active) {
// tx
await deleteOrgMembershipFn({
orgMembershipId: membership.id,
orgId: membership.orgId,
@@ -481,7 +517,7 @@ export const scimServiceFactory = ({
return buildScimUser({
orgMembershipId: membership.id,
username: membership.username,
username: membership.externalId ?? membership.username,
email: membership.email,
firstName: membership.firstName as string,
lastName: membership.lastName as string,
@@ -627,9 +663,9 @@ export const scimServiceFactory = ({
});
const orgMemberships = await orgDAL.findMembership({
orgId,
[`${TableName.OrgMembership}.orgId` as "orgId"]: orgId,
$in: {
userId: newGroup.newMembers.map((member) => member.id)
[`${TableName.OrgMembership}.userId` as "userId"]: newGroup.newMembers.map((member) => member.id)
}
});
@@ -668,9 +704,11 @@ export const scimServiceFactory = ({
});
const orgMemberships = await orgDAL.findMembership({
orgId,
[`${TableName.OrgMembership}.orgId` as "orgId"]: orgId,
$in: {
userId: users.filter((user) => user.isPartOfGroup).map((user) => user.id)
[`${TableName.OrgMembership}.userId` as "userId"]: users
.filter((user) => user.isPartOfGroup)
.map((user) => user.id)
}
});
+2 -2
View File
@@ -32,7 +32,7 @@ export type TGetScimUserDTO = {
};
export type TCreateScimUserDTO = {
username: string;
externalId: string;
email?: string;
firstName: string;
lastName: string;
@@ -40,7 +40,7 @@ export type TCreateScimUserDTO = {
};
export type TUpdateScimUserDTO = {
userId: string;
orgMembershipId: string;
orgId: string;
operations: {
op: string;