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 -1
View File
@@ -18,6 +18,7 @@ import { LdapConfigsSchema, LdapGroupMapsSchema } from "@app/db/schemas";
import { TLDAPConfig } from "@app/ee/services/ldap-config/ldap-config-types"; import { TLDAPConfig } from "@app/ee/services/ldap-config/ldap-config-types";
import { isValidLdapFilter, searchGroups } from "@app/ee/services/ldap-config/ldap-fns"; import { isValidLdapFilter, searchGroups } from "@app/ee/services/ldap-config/ldap-fns";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
@@ -52,6 +53,7 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => {
// eslint-disable-next-line // eslint-disable-next-line
async (req: IncomingMessage, user, cb) => { async (req: IncomingMessage, user, cb) => {
try { try {
if (!user.email) throw new BadRequestError({ message: "Invalid request. Missing email." });
const ldapConfig = (req as unknown as FastifyRequest).ldapConfig as TLDAPConfig; const ldapConfig = (req as unknown as FastifyRequest).ldapConfig as TLDAPConfig;
let groups: { dn: string; cn: string }[] | undefined; let groups: { dn: string; cn: string }[] | undefined;
@@ -74,7 +76,7 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => {
username: user.uid, username: user.uid,
firstName: user.givenName ?? user.cn ?? "", firstName: user.givenName ?? user.cn ?? "",
lastName: user.sn ?? "", lastName: user.sn ?? "",
emails: user.mail ? [user.mail] : [], email: user.mail,
groups, groups,
relayState: ((req as unknown as FastifyRequest).body as { RelayState?: string }).RelayState, relayState: ((req as unknown as FastifyRequest).body as { RelayState?: string }).RelayState,
orgId: (req as unknown as FastifyRequest).ldapConfig.organization orgId: (req as unknown as FastifyRequest).ldapConfig.organization
+1 -1
View File
@@ -102,7 +102,7 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
if (!profile) throw new BadRequestError({ message: "Missing profile" }); if (!profile) throw new BadRequestError({ message: "Missing profile" });
const email = profile?.email ?? (profile?.emailAddress as string); // emailRippling is added because in Rippling the field `email` reserved const email = profile?.email ?? (profile?.emailAddress as string); // emailRippling is added because in Rippling the field `email` reserved
if (!profile.email || !profile.firstName) { if (!email || !profile.firstName) {
throw new BadRequestError({ message: "Invalid request. Missing email or first name" }); throw new BadRequestError({ message: "Invalid request. Missing email or first name" });
} }
+1 -1
View File
@@ -249,7 +249,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
const primaryEmail = req.body.emails?.find((email) => email.primary)?.value; const primaryEmail = req.body.emails?.find((email) => email.primary)?.value;
const user = await req.server.services.scim.createScimUser({ const user = await req.server.services.scim.createScimUser({
username: req.body.userName, externalId: req.body.userName,
email: primaryEmail, email: primaryEmail,
firstName: req.body.name.givenName, firstName: req.body.name.givenName,
lastName: req.body.name.familyName, lastName: req.body.name.familyName,
+3 -3
View File
@@ -1,6 +1,6 @@
import { Knex } from "knex"; 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 { decryptAsymmetric, encryptAsymmetric, infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
import { BadRequestError, ScimRequestError } from "@app/lib/errors"; 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 // check if all user(s) are part of the organization
const existingUserOrgMemberships = await orgDAL.findMembership( const existingUserOrgMemberships = await orgDAL.findMembership(
{ {
orgId: group.orgId, [`${TableName.OrgMembership}.orgId` as "orgId"]: group.orgId,
$in: { $in: {
userId: userIds [`${TableName.OrgMembership}.userId` as "userId"]: userIds
} }
}, },
{ tx } { tx }
@@ -6,7 +6,8 @@ import {
OrgMembershipStatus, OrgMembershipStatus,
SecretKeyEncoding, SecretKeyEncoding,
TableName, TableName,
TLdapConfigsUpdate TLdapConfigsUpdate,
TUsers
} from "@app/db/schemas"; } from "@app/db/schemas";
import { TGroupDALFactory } from "@app/ee/services/group/group-dal"; import { TGroupDALFactory } from "@app/ee/services/group/group-dal";
import { addUsersToGroupByUserIds, removeUsersFromGroupByUserIds } from "@app/ee/services/group/group-fns"; 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 { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal"; import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal";
import { TOrgDALFactory } from "@app/services/org/org-dal"; import { TOrgDALFactory } from "@app/services/org/org-dal";
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal"; import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal"; import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal";
@@ -54,6 +56,7 @@ import { TLdapGroupMapDALFactory } from "./ldap-group-map-dal";
type TLdapConfigServiceFactoryDep = { type TLdapConfigServiceFactoryDep = {
ldapConfigDAL: Pick<TLdapConfigDALFactory, "create" | "update" | "findOne">; ldapConfigDAL: Pick<TLdapConfigDALFactory, "create" | "update" | "findOne">;
ldapGroupMapDAL: Pick<TLdapGroupMapDALFactory, "find" | "create" | "delete" | "findLdapGroupMapsByLdapConfigId">; ldapGroupMapDAL: Pick<TLdapGroupMapDALFactory, "find" | "create" | "delete" | "findLdapGroupMapsByLdapConfigId">;
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "create">;
orgDAL: Pick< orgDAL: Pick<
TOrgDALFactory, TOrgDALFactory,
"createMembership" | "updateMembershipById" | "findMembership" | "findOrgById" | "findOne" | "updateById" "createMembership" | "updateMembershipById" | "findMembership" | "findOrgById" | "findOne" | "updateById"
@@ -83,6 +86,7 @@ export const ldapConfigServiceFactory = ({
ldapConfigDAL, ldapConfigDAL,
ldapGroupMapDAL, ldapGroupMapDAL,
orgDAL, orgDAL,
orgMembershipDAL,
orgBotDAL, orgBotDAL,
groupDAL, groupDAL,
groupProjectDAL, groupProjectDAL,
@@ -387,7 +391,7 @@ export const ldapConfigServiceFactory = ({
username, username,
firstName, firstName,
lastName, lastName,
emails, email,
groups, groups,
orgId, orgId,
relayState relayState
@@ -407,7 +411,7 @@ export const ldapConfigServiceFactory = ({
await userDAL.transaction(async (tx) => { await userDAL.transaction(async (tx) => {
const [orgMembership] = await orgDAL.findMembership( const [orgMembership] = await orgDAL.findMembership(
{ {
userId: userAlias.userId, [`${TableName.OrgMembership}.userId` as "userId"]: userAlias.userId,
[`${TableName.OrgMembership}.orgId` as "id"]: orgId [`${TableName.OrgMembership}.orgId` as "id"]: orgId
}, },
{ tx } { tx }
@@ -434,41 +438,75 @@ export const ldapConfigServiceFactory = ({
}); });
} else { } else {
userAlias = await userDAL.transaction(async (tx) => { userAlias = await userDAL.transaction(async (tx) => {
const uniqueUsername = await normalizeUsername(username, userDAL); let newUser: TUsers | undefined;
const newUser = await userDAL.create( if (serverCfg.trustSamlEmails) {
{ newUser = await userDAL.findOne(
username: uniqueUsername, {
email: emails[0], email,
isEmailVerified: serverCfg.trustLdapEmails, isEmailVerified: true
firstName, },
lastName, tx
authMethods: [], );
isGhost: false }
},
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( const newUserAlias = await userAliasDAL.create(
{ {
userId: newUser.id, userId: newUser.id,
username, username,
aliasType: UserAliasType.LDAP, aliasType: UserAliasType.LDAP,
externalId, externalId,
emails, emails: [email],
orgId orgId
}, },
tx tx
); );
await orgDAL.createMembership( const [orgMembership] = await orgDAL.findMembership(
{ {
userId: newUser.id, [`${TableName.OrgMembership}.userId` as "userId"]: newUser.id,
orgId, [`${TableName.OrgMembership}.orgId` as "id"]: orgId
role: OrgMembershipRole.Member,
status: OrgMembershipStatus.Invited
}, },
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; return newUserAlias;
}); });
} }
@@ -51,7 +51,7 @@ export type TLdapLoginDTO = {
username: string; username: string;
firstName: string; firstName: string;
lastName: string; lastName: string;
emails: string[]; email: string;
orgId: string; orgId: string;
groups?: { groups?: {
dn: string; dn: string;
@@ -347,7 +347,7 @@ export const samlConfigServiceFactory = ({
const foundUser = await userDAL.findById(userAlias.userId, tx); const foundUser = await userDAL.findById(userAlias.userId, tx);
const [orgMembership] = await orgDAL.findMembership( const [orgMembership] = await orgDAL.findMembership(
{ {
userId: foundUser.id, [`${TableName.OrgMembership}.userId` as "userId"]: foundUser.id,
[`${TableName.OrgMembership}.orgId` as "id"]: orgId [`${TableName.OrgMembership}.orgId` as "id"]: orgId
}, },
{ tx } { tx }
@@ -378,19 +378,33 @@ export const samlConfigServiceFactory = ({
}); });
} else { } else {
user = await userDAL.transaction(async (tx) => { user = await userDAL.transaction(async (tx) => {
const uniqueUsername = await normalizeUsername(`${firstName ?? ""}-${lastName ?? ""}`, userDAL); let newUser: TUsers | undefined;
const newUser = await userDAL.create( if (serverCfg.trustSamlEmails) {
{ newUser = await userDAL.findOne(
username: uniqueUsername, {
email, email,
isEmailVerified: serverCfg.trustSamlEmails, isEmailVerified: true
firstName, },
lastName, tx
authMethods: [], );
isGhost: false }
},
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( await userAliasDAL.create(
{ {
userId: newUser.id, userId: newUser.id,
@@ -402,17 +416,36 @@ export const samlConfigServiceFactory = ({
tx tx
); );
await orgMembershipDAL.create( const [orgMembership] = await orgDAL.findMembership(
{ {
userId: newUser.id, [`${TableName.OrgMembership}.userId` as "userId"]: newUser.id,
inviteEmail: email, [`${TableName.OrgMembership}.orgId` as "id"]: orgId
orgId,
role: OrgMembershipRole.Member,
status: OrgMembershipStatus.Invited
}, },
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; return newUser;
}); });
} }
@@ -46,7 +46,7 @@ export type TGetSamlCfgDTO =
export type TSamlLoginDTO = { export type TSamlLoginDTO = {
externalId: string; externalId: string;
email?: string; email: string;
firstName: string; firstName: string;
lastName?: string; lastName?: string;
authProvider: string; authProvider: string;
+76 -38
View File
@@ -209,10 +209,10 @@ export const scimServiceFactory = ({
findOpts findOpts
); );
const scimUsers = users.map(({ id, username, firstName, lastName, email }) => const scimUsers = users.map(({ id, externalId, username, firstName, lastName, email }) =>
buildScimUser({ buildScimUser({
orgMembershipId: id ?? "", orgMembershipId: id ?? "",
username, username: externalId ?? username,
firstName: firstName ?? "", firstName: firstName ?? "",
lastName: lastName ?? "", lastName: lastName ?? "",
email, email,
@@ -254,7 +254,7 @@ export const scimServiceFactory = ({
return buildScimUser({ return buildScimUser({
orgMembershipId: membership.id, orgMembershipId: membership.id,
username: membership.username, username: membership.externalId ?? membership.username,
email: membership.email ?? "", email: membership.email ?? "",
firstName: membership.firstName as string, firstName: membership.firstName as string,
lastName: membership.lastName 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); const org = await orgDAL.findById(orgId);
if (!org) if (!org)
@@ -281,13 +283,13 @@ export const scimServiceFactory = ({
const serverCfg = await getServerCfg(); const serverCfg = await getServerCfg();
const userAlias = await userAliasDAL.findOne({ const userAlias = await userAliasDAL.findOne({
externalId: username, externalId,
orgId, orgId,
aliasType: UserAliasType.SAML aliasType: UserAliasType.SAML
}); });
const { user: createdUser, orgMembership: createdOrgMembership } = await userDAL.transaction(async (tx) => { const { user: createdUser, orgMembership: createdOrgMembership } = await userDAL.transaction(async (tx) => {
let user: TUsers; let user: TUsers | undefined;
let orgMembership: TOrgMemberships; let orgMembership: TOrgMemberships;
if (userAlias) { if (userAlias) {
user = await userDAL.findById(userAlias.userId, tx); user = await userDAL.findById(userAlias.userId, tx);
@@ -320,39 +322,74 @@ export const scimServiceFactory = ({
); );
} }
} else { } else {
const uniqueUsername = await normalizeUsername(`${firstName}-${lastName}`, userDAL); if (serverCfg.trustSamlEmails) {
user = await userDAL.create( user = await userDAL.findOne(
{ {
username: uniqueUsername, email,
email, isEmailVerified: true
isEmailVerified: serverCfg.trustSamlEmails, },
firstName, tx
lastName, );
authMethods: [], }
isGhost: false
}, if (!user) {
tx 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( await userAliasDAL.create(
{ {
userId: user.id, userId: user.id,
aliasType: UserAliasType.SAML, aliasType: UserAliasType.SAML,
externalId: username, externalId,
emails: email ? [email] : [], emails: email ? [email] : [],
orgId orgId
}, },
tx tx
); );
orgMembership = await orgMembershipDAL.create(
const [foundOrgMembership] = await orgDAL.findMembership(
{ {
userId: user.id, [`${TableName.OrgMembership}.userId` as "userId"]: user.id,
inviteEmail: email, [`${TableName.OrgMembership}.orgId` as "id"]: orgId
orgId,
role: OrgMembershipRole.Member,
status: OrgMembershipStatus.Invited
}, },
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 }; return { user, orgMembership };
@@ -372,7 +409,7 @@ export const scimServiceFactory = ({
return buildScimUser({ return buildScimUser({
orgMembershipId: createdOrgMembership.id, orgMembershipId: createdOrgMembership.id,
username: createdUser.username, username: externalId,
firstName: createdUser.firstName as string, firstName: createdUser.firstName as string,
lastName: createdUser.lastName as string, lastName: createdUser.lastName as string,
email: createdUser.email ?? "", 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 const [membership] = await orgDAL
.findMembership({ .findMembership({
userId, [`${TableName.OrgMembership}.id` as "id"]: orgMembershipId,
[`${TableName.OrgMembership}.orgId` as "id"]: orgId [`${TableName.OrgMembership}.orgId` as "orgId"]: orgId
}) })
.catch(() => { .catch(() => {
throw new ScimRequestError({ throw new ScimRequestError({
@@ -433,7 +470,7 @@ export const scimServiceFactory = ({
return buildScimUser({ return buildScimUser({
orgMembershipId: membership.id, orgMembershipId: membership.id,
username: membership.username, username: membership.externalId ?? membership.username,
email: membership.email, email: membership.email,
firstName: membership.firstName as string, firstName: membership.firstName as string,
lastName: membership.lastName as string, lastName: membership.lastName as string,
@@ -467,7 +504,6 @@ export const scimServiceFactory = ({
}); });
if (!active) { if (!active) {
// tx
await deleteOrgMembershipFn({ await deleteOrgMembershipFn({
orgMembershipId: membership.id, orgMembershipId: membership.id,
orgId: membership.orgId, orgId: membership.orgId,
@@ -481,7 +517,7 @@ export const scimServiceFactory = ({
return buildScimUser({ return buildScimUser({
orgMembershipId: membership.id, orgMembershipId: membership.id,
username: membership.username, username: membership.externalId ?? membership.username,
email: membership.email, email: membership.email,
firstName: membership.firstName as string, firstName: membership.firstName as string,
lastName: membership.lastName as string, lastName: membership.lastName as string,
@@ -627,9 +663,9 @@ export const scimServiceFactory = ({
}); });
const orgMemberships = await orgDAL.findMembership({ const orgMemberships = await orgDAL.findMembership({
orgId, [`${TableName.OrgMembership}.orgId` as "orgId"]: orgId,
$in: { $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({ const orgMemberships = await orgDAL.findMembership({
orgId, [`${TableName.OrgMembership}.orgId` as "orgId"]: orgId,
$in: { $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 = { export type TCreateScimUserDTO = {
username: string; externalId: string;
email?: string; email?: string;
firstName: string; firstName: string;
lastName: string; lastName: string;
@@ -40,7 +40,7 @@ export type TCreateScimUserDTO = {
}; };
export type TUpdateScimUserDTO = { export type TUpdateScimUserDTO = {
userId: string; orgMembershipId: string;
orgId: string; orgId: string;
operations: { operations: {
op: string; op: string;
+1
View File
@@ -322,6 +322,7 @@ export const registerRoutes = async (
ldapConfigDAL, ldapConfigDAL,
ldapGroupMapDAL, ldapGroupMapDAL,
orgDAL, orgDAL,
orgMembershipDAL,
orgBotDAL, orgBotDAL,
groupDAL, groupDAL,
groupProjectDAL, groupProjectDAL,
@@ -1,6 +1,6 @@
import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
import { OrgMembershipStatus } from "@app/db/schemas"; import { OrgMembershipStatus, TableName } from "@app/db/schemas";
import { convertPendingGroupAdditionsToGroupMemberships } from "@app/ee/services/group/group-fns"; import { convertPendingGroupAdditionsToGroupMemberships } from "@app/ee/services/group/group-fns";
import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal"; import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
@@ -102,6 +102,8 @@ export const authSignupServiceFactory = ({
code code
}); });
await userDAL.updateById(user.id, { isEmailVerified: true });
// generate jwt token this is a temporary token // generate jwt token this is a temporary token
const jwtToken = jwt.sign( const jwtToken = jwt.sign(
{ {
@@ -171,9 +173,9 @@ export const authSignupServiceFactory = ({
// If it's SAML Auth and the organization ID is present, we should check if the user has a pending invite for this org, and accept it // If it's SAML Auth and the organization ID is present, we should check if the user has a pending invite for this org, and accept it
if ((isAuthMethodSaml(authMethod) || authMethod === AuthMethod.LDAP) && organizationId) { if ((isAuthMethodSaml(authMethod) || authMethod === AuthMethod.LDAP) && organizationId) {
const [pendingOrgMembership] = await orgDAL.findMembership({ const [pendingOrgMembership] = await orgDAL.findMembership({
userId: user.id, [`${TableName.OrgMembership}.userId` as "userId"]: user.id,
status: OrgMembershipStatus.Invited, status: OrgMembershipStatus.Invited,
orgId: organizationId [`${TableName.OrgMembership}.orgId` as "orgId"]: organizationId
}); });
if (pendingOrgMembership) { if (pendingOrgMembership) {
+7 -1
View File
@@ -262,13 +262,19 @@ export const orgDALFactory = (db: TDbClient) => {
.where(buildFindFilter(filter)) .where(buildFindFilter(filter))
.join(TableName.Users, `${TableName.Users}.id`, `${TableName.OrgMembership}.userId`) .join(TableName.Users, `${TableName.Users}.id`, `${TableName.OrgMembership}.userId`)
.join(TableName.Organization, `${TableName.Organization}.id`, `${TableName.OrgMembership}.orgId`) .join(TableName.Organization, `${TableName.Organization}.id`, `${TableName.OrgMembership}.orgId`)
.leftJoin(TableName.UserAliases, function joinUserAlias() {
this.on(`${TableName.UserAliases}.userId`, "=", `${TableName.OrgMembership}.userId`)
.andOn(`${TableName.UserAliases}.orgId`, "=", `${TableName.OrgMembership}.orgId`)
.andOn(`${TableName.UserAliases}.aliasType`, "=", (tx || db).raw("?", ["saml"]));
})
.select( .select(
selectAllTableCols(TableName.OrgMembership), selectAllTableCols(TableName.OrgMembership),
db.ref("email").withSchema(TableName.Users), db.ref("email").withSchema(TableName.Users),
db.ref("username").withSchema(TableName.Users), db.ref("username").withSchema(TableName.Users),
db.ref("firstName").withSchema(TableName.Users), db.ref("firstName").withSchema(TableName.Users),
db.ref("lastName").withSchema(TableName.Users), db.ref("lastName").withSchema(TableName.Users),
db.ref("scimEnabled").withSchema(TableName.Organization) db.ref("scimEnabled").withSchema(TableName.Organization),
db.ref("externalId").withSchema(TableName.UserAliases)
) )
.where({ isGhost: false }); .where({ isGhost: false });
+10 -4
View File
@@ -4,7 +4,7 @@ import crypto from "crypto";
import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
import { Knex } from "knex"; import { Knex } from "knex";
import { OrgMembershipRole, OrgMembershipStatus } from "@app/db/schemas"; import { OrgMembershipRole, OrgMembershipStatus, TableName } from "@app/db/schemas";
import { TProjects } from "@app/db/schemas/projects"; import { TProjects } from "@app/db/schemas/projects";
import { TGroupDALFactory } from "@app/ee/services/group/group-dal"; import { TGroupDALFactory } from "@app/ee/services/group/group-dal";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
@@ -431,7 +431,13 @@ export const orgServiceFactory = ({
if (inviteeUser) { if (inviteeUser) {
// if user already exist means its already part of infisical // if user already exist means its already part of infisical
// Thus the signup flow is not needed anymore // Thus the signup flow is not needed anymore
const [inviteeMembership] = await orgDAL.findMembership({ orgId, userId: inviteeUser.id }, { tx }); const [inviteeMembership] = await orgDAL.findMembership(
{
[`${TableName.OrgMembership}.orgId` as "orgId"]: orgId,
[`${TableName.OrgMembership}.userId` as "userId"]: inviteeUser.id
},
{ tx }
);
if (inviteeMembership && inviteeMembership.status === OrgMembershipStatus.Accepted) { if (inviteeMembership && inviteeMembership.status === OrgMembershipStatus.Accepted) {
throw new BadRequestError({ throw new BadRequestError({
message: "Failed to invite an existing member of org", message: "Failed to invite an existing member of org",
@@ -523,9 +529,9 @@ export const orgServiceFactory = ({
throw new BadRequestError({ message: "Invalid request", name: "Verify user to org" }); throw new BadRequestError({ message: "Invalid request", name: "Verify user to org" });
} }
const [orgMembership] = await orgDAL.findMembership({ const [orgMembership] = await orgDAL.findMembership({
userId: user.id, [`${TableName.OrgMembership}.userId` as "userId"]: user.id,
status: OrgMembershipStatus.Invited, status: OrgMembershipStatus.Invited,
orgId [`${TableName.OrgMembership}.orgId` as "orgId"]: orgId
}); });
if (!orgMembership) if (!orgMembership)
throw new BadRequestError({ throw new BadRequestError({
@@ -110,7 +110,7 @@ export const projectMembershipServiceFactory = ({
); );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Member); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Member);
const orgMembers = await orgDAL.findMembership({ const orgMembers = await orgDAL.findMembership({
orgId: project.orgId, [`${TableName.OrgMembership}.orgId` as "orgId"]: project.orgId,
$in: { $in: {
[`${TableName.OrgMembership}.id` as "id"]: members.map(({ orgMembershipId }) => orgMembershipId) [`${TableName.OrgMembership}.id` as "id"]: members.map(({ orgMembershipId }) => orgMembershipId)
} }
@@ -119,7 +119,7 @@ export const projectMembershipServiceFactory = ({
const existingMembers = await projectMembershipDAL.find({ const existingMembers = await projectMembershipDAL.find({
projectId, projectId,
$in: { userId: orgMembers.map(({ userId }) => userId).filter(Boolean) as string[] } $in: { userId: orgMembers.map(({ userId }) => userId).filter(Boolean) }
}); });
if (existingMembers.length) throw new BadRequestError({ message: "Some users are already part of project" }); if (existingMembers.length) throw new BadRequestError({ message: "Some users are already part of project" });
@@ -134,7 +134,7 @@ export const projectMembershipServiceFactory = ({
const projectMemberships = await projectMembershipDAL.insertMany( const projectMemberships = await projectMembershipDAL.insertMany(
orgMembers.map(({ userId }) => ({ orgMembers.map(({ userId }) => ({
projectId, projectId,
userId: userId as string userId
})), })),
tx tx
); );
@@ -145,12 +145,12 @@ export const projectMembershipServiceFactory = ({
const encKeyGroupByOrgMembId = groupBy(members, (i) => i.orgMembershipId); const encKeyGroupByOrgMembId = groupBy(members, (i) => i.orgMembershipId);
await projectKeyDAL.insertMany( await projectKeyDAL.insertMany(
orgMembers orgMembers
.filter(({ userId }) => !userIdsToExcludeForProjectKeyAddition.has(userId as string)) .filter(({ userId }) => !userIdsToExcludeForProjectKeyAddition.has(userId))
.map(({ userId, id }) => ({ .map(({ userId, id }) => ({
encryptedKey: encKeyGroupByOrgMembId[id][0].workspaceEncryptedKey, encryptedKey: encKeyGroupByOrgMembId[id][0].workspaceEncryptedKey,
nonce: encKeyGroupByOrgMembId[id][0].workspaceEncryptedNonce, nonce: encKeyGroupByOrgMembId[id][0].workspaceEncryptedNonce,
senderId: actorId, senderId: actorId,
receiverId: userId as string, receiverId: userId,
projectId projectId
})), })),
tx tx
@@ -8,6 +8,7 @@ import {
SecretKeyEncoding, SecretKeyEncoding,
SecretsSchema, SecretsSchema,
SecretVersionsSchema, SecretVersionsSchema,
TableName,
TIntegrationAuths, TIntegrationAuths,
TSecretApprovalRequestsSecrets, TSecretApprovalRequestsSecrets,
TSecrets, TSecrets,
@@ -273,7 +274,10 @@ export const projectQueueFactory = ({
for (const key of existingProjectKeys) { for (const key of existingProjectKeys) {
const user = await userDAL.findUserEncKeyByUserId(key.receiverId); const user = await userDAL.findUserEncKeyByUserId(key.receiverId);
const [orgMembership] = await orgDAL.findMembership({ userId: key.receiverId, orgId: project.orgId }); const [orgMembership] = await orgDAL.findMembership({
[`${TableName.OrgMembership}.userId` as "userId"]: key.receiverId,
[`${TableName.OrgMembership}.orgId` as "orgId"]: project.orgId
});
if (!user) { if (!user) {
throw new Error(`User with ID ${key.receiverId} was not found during upgrade.`); throw new Error(`User with ID ${key.receiverId} was not found during upgrade.`);
+14 -1
View File
@@ -63,6 +63,8 @@ export const userServiceFactory = ({
const verifyEmailVerificationCode = async (username: string, code: string) => { const verifyEmailVerificationCode = async (username: string, code: string) => {
const user = await userDAL.findOne({ username }); const user = await userDAL.findOne({ username });
if (!user) throw new BadRequestError({ name: "Failed to find user" }); if (!user) throw new BadRequestError({ name: "Failed to find user" });
if (!user.email)
throw new BadRequestError({ name: "Failed to verify email verification code due to no email on user" });
if (user.isEmailVerified) if (user.isEmailVerified)
throw new BadRequestError({ name: "Failed to verify email verification code due to email already verified" }); throw new BadRequestError({ name: "Failed to verify email verification code due to email already verified" });
@@ -72,6 +74,8 @@ export const userServiceFactory = ({
code code
}); });
const { email } = user;
await userDAL.transaction(async (tx) => { await userDAL.transaction(async (tx) => {
await userDAL.updateById( await userDAL.updateById(
user.id, user.id,
@@ -84,7 +88,7 @@ export const userServiceFactory = ({
// check if there are users with the same email. // check if there are users with the same email.
const users = await userDAL.find( const users = await userDAL.find(
{ {
email: user.email, email,
isEmailVerified: true isEmailVerified: true
}, },
{ tx } { tx }
@@ -129,6 +133,15 @@ export const userServiceFactory = ({
tx tx
); );
} }
} else {
// update current user's username to [email]
await userDAL.updateById(
user.id,
{
username: email
},
tx
);
} }
}); });
}; };
@@ -12,6 +12,10 @@ description: "Learn how to log in to Infisical with LDAP."
You can configure your organization in Infisical to have members authenticate with the platform via [LDAP](https://en.wikipedia.org/wiki/Lightweight_Directory_Access_Protocol) You can configure your organization in Infisical to have members authenticate with the platform via [LDAP](https://en.wikipedia.org/wiki/Lightweight_Directory_Access_Protocol)
Prerequisites:
- You must have an email address to use LDAP, regardless of whether or not you use that email address to sign in.
<Steps> <Steps>
<Step title="Prepare the LDAP configuration in Infisical"> <Step title="Prepare the LDAP configuration in Infisical">
In Infisical, head to your Organization Settings > Security > LDAP and select **Manage**. In Infisical, head to your Organization Settings > Security > LDAP and select **Manage**.
@@ -10,6 +10,10 @@ description: "Learn how to configure JumpCloud LDAP for authenticating into Infi
it. it.
</Info> </Info>
Prerequisites:
- You must have an email address to use LDAP, regardless of whether or not you use that email address to sign in.
<Steps> <Steps>
<Step title="Prepare LDAP in JumpCloud"> <Step title="Prepare LDAP in JumpCloud">
In JumpCloud, head to USER MANAGEMENT > Users and create a new user via the **Manual user entry** option. This user In JumpCloud, head to USER MANAGEMENT > Users and create a new user via the **Manual user entry** option. This user
+19 -2
View File
@@ -3,11 +3,13 @@ title: "LDAP Overview"
sidebarTitle: "Overview" sidebarTitle: "Overview"
description: "Learn how to authenticate into Infisical with LDAP." description: "Learn how to authenticate into Infisical with LDAP."
--- ---
<Info> <Info>
LDAP is a paid feature. LDAP is a paid feature.
If you're using Infisical Cloud, then it is available under the **Enterprise Tier**. If you're self-hosting Infisical, If you're using Infisical Cloud, then it is available under the **Enterprise Tier**. If you're self-hosting Infisical,
then you should contact [email protected] to purchase an enterprise license to use it. then you should contact [email protected] to purchase an enterprise license to use it.
</Info> </Info>
You can configure your organization in Infisical to have members authenticate with the platform via [LDAP](https://en.wikipedia.org/wiki/Lightweight_Directory_Access_Protocol). You can configure your organization in Infisical to have members authenticate with the platform via [LDAP](https://en.wikipedia.org/wiki/Lightweight_Directory_Access_Protocol).
@@ -25,3 +27,18 @@ Read the general instructions for configuring LDAP [here](/documentation/platfor
If the documentation for your required identity provider is not shown in the list above, please reach out to [[email protected]](mailto:[email protected]) for assistance. If the documentation for your required identity provider is not shown in the list above, please reach out to [[email protected]](mailto:[email protected]) for assistance.
## FAQ
<AccordionGroup>
<Accordion title="Why does Infisical require additional email verification for users connected via LDAP?">
By default, Infisical Cloud is configured to not trust emails from external
identity providers to prevent any malicious account takeover attempts via
email spoofing. Accordingly, Infisical creates a new user for anyone provisioned
through an external identity provider and requires an additional email
verification step upon their first login.
If you're running a self-hosted instance of Infisical and would like it to trust emails from external identity providers,
you can configure this behavior in the admin panel.
</Accordion>
</AccordionGroup>
+16 -9
View File
@@ -4,10 +4,10 @@ description: "Learn how to configure Okta SAML 2.0 for Infisical SSO."
--- ---
<Info> <Info>
Okta SAML SSO is a paid feature. Okta SAML SSO is a paid feature. If you're using Infisical Cloud, then it is
available under the **Pro Tier**. If you're self-hosting Infisical, then you
If you're using Infisical Cloud, then it is available under the **Pro Tier**. If you're self-hosting Infisical, should contact [email protected] to purchase an enterprise license to use
then you should contact [email protected] to purchase an enterprise license to use it. it.
</Info> </Info>
<Steps> <Steps>
@@ -50,6 +50,7 @@ description: "Learn how to configure Okta SAML 2.0 for Infisical SSO."
![SAML Okta attribute statements](../../../images/sso/okta/attribute-statements.png) ![SAML Okta attribute statements](../../../images/sso/okta/attribute-statements.png)
Once configured, select **Next** to proceed to the Feedback screen and select **Finish**. Once configured, select **Next** to proceed to the Feedback screen and select **Finish**.
</Step> </Step>
<Step title="Retrieve Identity Provider (IdP) Information from Okta"> <Step title="Retrieve Identity Provider (IdP) Information from Okta">
Once your application is created, select the **Sign On** tab for the app and select the **View Setup Instructions** button located on the right side of the screen: Once your application is created, select the **Sign On** tab for the app and select the **View Setup Instructions** button located on the right side of the screen:
@@ -59,12 +60,14 @@ description: "Learn how to configure Okta SAML 2.0 for Infisical SSO."
Copy the **Identity Provider Single Sign-On URL**, the **Identity Provider Issuer**, and the **X.509 Certificate** to use when finishing configuring Okta SAML in Infisical. Copy the **Identity Provider Single Sign-On URL**, the **Identity Provider Issuer**, and the **X.509 Certificate** to use when finishing configuring Okta SAML in Infisical.
![SAML Okta IdP values](../../../images/sso/okta/idp-values.png) ![SAML Okta IdP values](../../../images/sso/okta/idp-values.png)
</Step> </Step>
<Step title="Finish configuring SAML in Infisical"> <Step title="Finish configuring SAML in Infisical">
Back in Infisical, set **Identity Provider Single Sign-On URL**, **Identity Provider Issuer**, Back in Infisical, set **Identity Provider Single Sign-On URL**, **Identity Provider Issuer**,
and **Certificate** to **X.509 Certificate** from step 3. Once you've done that, press **Update** to complete the required configuration. and **Certificate** to **X.509 Certificate** from step 3. Once you've done that, press **Update** to complete the required configuration.
![SAML Okta paste values into Infisical](../../../images/sso/okta/idp-values-2.png) ![SAML Okta paste values into Infisical](../../../images/sso/okta/idp-values-2.png)
</Step> </Step>
<Step title="Assign users in Okta to the application"> <Step title="Assign users in Okta to the application">
Back in Okta, navigate to the **Assignments** tab and select **Assign**. You can assign access to the application on a user-by-user basis using the Assign to People option, or in-bulk using the Assign to Groups option. Back in Okta, navigate to the **Assignments** tab and select **Assign**. You can assign access to the application on a user-by-user basis using the Assign to People option, or in-bulk using the Assign to Groups option.
@@ -72,11 +75,13 @@ description: "Learn how to configure Okta SAML 2.0 for Infisical SSO."
![SAML Okta assignment](../../../images/sso/okta/assignment.png) ![SAML Okta assignment](../../../images/sso/okta/assignment.png)
At this point, you have configured everything you need within the context of the Okta Admin Portal. At this point, you have configured everything you need within the context of the Okta Admin Portal.
</Step> </Step>
<Step title="Enable SAML SSO in Infisical"> <Step title="Enable SAML SSO in Infisical">
Enabling SAML SSO allows members in your organization to log into Infisical via Okta. Enabling SAML SSO allows members in your organization to log into Infisical via Okta.
![SAML Okta enable SAML](../../../images/sso/okta/enable-saml.png) ![SAML Okta enable SAML](../../../images/sso/okta/enable-saml.png)
</Step> </Step>
<Step title="Enforce SAML SSO in Infisical"> <Step title="Enforce SAML SSO in Infisical">
Enforcing SAML SSO ensures that members in your organization can only access Infisical Enforcing SAML SSO ensures that members in your organization can only access Infisical
@@ -89,13 +94,15 @@ description: "Learn how to configure Okta SAML 2.0 for Infisical SSO."
We recommend ensuring that your account is provisioned the application in Okta We recommend ensuring that your account is provisioned the application in Okta
prior to enforcing SAML SSO to prevent any unintended issues. prior to enforcing SAML SSO to prevent any unintended issues.
</Warning> </Warning>
</Step> </Step>
</Steps> </Steps>
<Note> <Note>
If you're configuring SAML SSO on a self-hosted instance of Infisical, make sure to If you're configuring SAML SSO on a self-hosted instance of Infisical, make
set the `AUTH_SECRET` and `SITE_URL` environment variable for it to work: sure to set the `AUTH_SECRET` and `SITE_URL` environment variable for it to
work: - `AUTH_SECRET`: A secret key used for signing and verifying JWT. This
- `AUTH_SECRET`: A secret key used for signing and verifying JWT. This can be a random 32-byte base64 string generated with `openssl rand -base64 32`. can be a random 32-byte base64 string generated with `openssl rand -base64
- `SITE_URL`: The URL of your self-hosted instance of Infisical - should be an absolute URL including the protocol (e.g. https://app.infisical.com) 32`. - `SITE_URL`: The URL of your self-hosted instance of Infisical - should
be an absolute URL including the protocol (e.g. https://app.infisical.com)
</Note> </Note>
+22 -5
View File
@@ -5,11 +5,12 @@ description: "Learn how to log in to Infisical via SSO protocols."
--- ---
<Info> <Info>
Infisical offers Google SSO and GitHub SSO for free across both Infisical Cloud and Infisical Self-hosted. Infisical offers Google SSO and GitHub SSO for free across both Infisical
Cloud and Infisical Self-hosted. Infisical also offers SAML SSO authentication
Infisical also offers SAML SSO authentication but as paid features that can be unlocked on Infisical Cloud's **Pro** tier but as paid features that can be unlocked on Infisical Cloud's **Pro** tier or
or via enterprise license on self-hosted instances of Infisical. On this front, we support industry-leading providers including via enterprise license on self-hosted instances of Infisical. On this front,
Okta, Azure AD, and JumpCloud; with any questions, please reach out to [email protected]. we support industry-leading providers including Okta, Azure AD, and JumpCloud;
with any questions, please reach out to [email protected].
</Info> </Info>
You can configure your organization in Infisical to have members authenticate with the platform via protocols like [SAML 2.0](https://en.wikipedia.org/wiki/SAML_2.0). You can configure your organization in Infisical to have members authenticate with the platform via protocols like [SAML 2.0](https://en.wikipedia.org/wiki/SAML_2.0).
@@ -31,3 +32,19 @@ Infisical supports these and many other identity providers:
- [Google SAML](/documentation/platform/sso/google-saml) - [Google SAML](/documentation/platform/sso/google-saml)
If your required identity provider is not shown in the list above, please reach out to [[email protected]](mailto:[email protected]) for assistance. If your required identity provider is not shown in the list above, please reach out to [[email protected]](mailto:[email protected]) for assistance.
## FAQ
<AccordionGroup>
<Accordion title="Why does Infisical require additional email verification for users connected via SAML?">
By default, Infisical Cloud is configured to not trust emails from external
identity providers to prevent any malicious account takeover attempts via
email spoofing. Accordingly, Infisical creates a new user for anyone provisioned
through an external identity provider and requires an additional email
verification step upon their first login.
If you're running a self-hosted instance of Infisical and would like it to trust emails from external identity providers,
you can configure this behavior in the admin panel.
</Accordion>
</AccordionGroup>
-8
View File
@@ -54,14 +54,6 @@ export const SignupSSO = ({ providerAuthToken }: Props) => {
providerAuthToken={providerAuthToken} providerAuthToken={providerAuthToken}
/> />
); );
// case 2:
// return (
// <MergeUsersStep
// username={username}
// authType={authType}
// organizationSlug={organizationSlug}
// />
// );
case 2: case 2:
return ( return (
<BackupPDFStep email={username} password={password} name={`${firstName} ${lastName}`} /> <BackupPDFStep email={username} password={password} name={`${firstName} ${lastName}`} />