Merge branch 'main' into ENG-2633

This commit is contained in:
x
2025-04-24 21:23:38 -04:00
15 changed files with 343 additions and 32 deletions
@@ -3,6 +3,8 @@ import jwt from "jsonwebtoken";
import { Knex } from "knex";
import { OrgMembershipRole, TUsers, UserDeviceSchema } from "@app/db/schemas";
import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns";
import { getConfig } from "@app/lib/config/env";
import { request } from "@app/lib/config/request";
@@ -11,6 +13,7 @@ import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { getUserPrivateKey } from "@app/lib/crypto/srp";
import { BadRequestError, DatabaseError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { getUserAgentType } from "@app/server/plugins/audit-log";
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
@@ -28,7 +31,15 @@ import {
TOauthTokenExchangeDTO,
TVerifyMfaTokenDTO
} from "./auth-login-type";
import { AuthMethod, AuthModeJwtTokenPayload, AuthModeMfaJwtTokenPayload, AuthTokenType, MfaMethod } from "./auth-type";
import {
ActorType,
AuthMethod,
AuthModeJwtTokenPayload,
AuthModeMfaJwtTokenPayload,
AuthTokenType,
MfaMethod
} from "./auth-type";
import { removeTrailingSlash } from "@app/lib/fn";
type TAuthLoginServiceFactoryDep = {
userDAL: TUserDALFactory;
@@ -36,6 +47,7 @@ type TAuthLoginServiceFactoryDep = {
tokenService: TAuthTokenServiceFactory;
smtpService: TSmtpService;
totpService: Pick<TTotpServiceFactory, "verifyUserTotp" | "verifyWithUserRecoveryCode">;
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
};
export type TAuthLoginFactory = ReturnType<typeof authLoginServiceFactory>;
@@ -44,7 +56,8 @@ export const authLoginServiceFactory = ({
tokenService,
smtpService,
orgDAL,
totpService
totpService,
auditLogService
}: TAuthLoginServiceFactoryDep) => {
/*
* Private
@@ -412,6 +425,55 @@ export const authLoginServiceFactory = ({
mfaMethod: decodedToken.mfaMethod
});
// In the event of this being a break-glass request (non-saml / non-oidc, when either is enforced)
if (
selectedOrg.authEnforced &&
selectedOrg.bypassOrgAuthEnabled &&
!isAuthMethodSaml(decodedToken.authMethod) &&
decodedToken.authMethod !== AuthMethod.OIDC
) {
await auditLogService.createAuditLog({
orgId: organizationId,
ipAddress,
userAgent,
userAgentType: getUserAgentType(userAgent),
actor: {
type: ActorType.USER,
metadata: {
email: user.email,
userId: user.id,
username: user.username
}
},
event: {
type: EventType.ORG_ADMIN_BYPASS_SSO,
metadata: {}
}
});
// Notify all admins via email (besides the actor)
const orgAdmins = await orgDAL.findOrgMembersByRole(organizationId, OrgMembershipRole.Admin);
const adminEmails = orgAdmins
.filter((admin) => admin.user.id !== user.id)
.map((admin) => admin.user.email)
.filter(Boolean) as string[];
if (adminEmails.length > 0) {
await smtpService.sendMail({
recipients: adminEmails,
subjectLine: "Security Alert: Admin SSO Bypass",
substitutions: {
email: user.email,
timestamp: new Date().toISOString(),
ip: ipAddress,
userAgent,
siteUrl: removeTrailingSlash(cfg.SITE_URL || "https://app.infisical.com")
},
template: SmtpTemplates.OrgAdminBreakglassAccess
});
}
}
return {
...tokens,
isMfaEnabled: false
+7 -1
View File
@@ -787,13 +787,19 @@ export const kmsServiceFactory = ({
return projectDataKey;
}
}
} catch (error) {
logger.error(
error,
`getProjectSecretManagerKmsDataKey: Failed to get project data key for [projectId=${projectId}]`
);
throw error;
} finally {
await lock?.release();
}
}
if (!project.kmsSecretManagerEncryptedDataKey) {
throw new Error("Missing project data key");
throw new BadRequestError({ message: "Missing project data key" });
}
const kmsDecryptor = await decryptWithKmsKey({
+40 -2
View File
@@ -2,6 +2,7 @@ import { Knex } from "knex";
import { TDbClient } from "@app/db";
import {
OrgMembershipRole,
TableName,
TOrganizations,
TOrganizationsInsert,
@@ -216,9 +217,8 @@ export const orgDALFactory = (db: TDbClient) => {
const findOrgMembersByUsername = async (orgId: string, usernames: string[], tx?: Knex) => {
try {
const conn = tx || db;
const conn = tx || db.replicaNode();
const members = await conn(TableName.OrgMembership)
// .replicaNode()(TableName.OrgMembership)
.where(`${TableName.OrgMembership}.orgId`, orgId)
.join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`)
.leftJoin<TUserEncryptionKeys>(
@@ -251,6 +251,43 @@ export const orgDALFactory = (db: TDbClient) => {
}
};
const findOrgMembersByRole = async (orgId: string, role: OrgMembershipRole, tx?: Knex) => {
try {
const conn = tx || db.replicaNode();
const members = await conn(TableName.OrgMembership)
.where(`${TableName.OrgMembership}.orgId`, orgId)
.where(`${TableName.OrgMembership}.role`, role)
.join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`)
.leftJoin<TUserEncryptionKeys>(
TableName.UserEncryptionKey,
`${TableName.UserEncryptionKey}.userId`,
`${TableName.Users}.id`
)
.select(
conn.ref("id").withSchema(TableName.OrgMembership),
conn.ref("inviteEmail").withSchema(TableName.OrgMembership),
conn.ref("orgId").withSchema(TableName.OrgMembership),
conn.ref("role").withSchema(TableName.OrgMembership),
conn.ref("roleId").withSchema(TableName.OrgMembership),
conn.ref("status").withSchema(TableName.OrgMembership),
conn.ref("username").withSchema(TableName.Users),
conn.ref("email").withSchema(TableName.Users),
conn.ref("firstName").withSchema(TableName.Users),
conn.ref("lastName").withSchema(TableName.Users),
conn.ref("id").withSchema(TableName.Users).as("userId"),
conn.ref("publicKey").withSchema(TableName.UserEncryptionKey)
)
.where({ isGhost: false });
return members.map(({ username, email, firstName, lastName, userId, publicKey, ...data }) => ({
...data,
user: { username, email, firstName, lastName, id: userId, publicKey }
}));
} catch (error) {
throw new DatabaseError({ error, name: "Find org members by role" });
}
};
const findOrgGhostUser = async (orgId: string) => {
try {
const member = await db
@@ -472,6 +509,7 @@ export const orgDALFactory = (db: TDbClient) => {
findAllOrgsByUserId,
ghostUserExists,
findOrgMembersByUsername,
findOrgMembersByRole,
findOrgGhostUser,
create,
updateById,
@@ -44,6 +44,7 @@ export enum SmtpTemplates {
SecretRotationFailed = "secretRotationFailed.handlebars",
ProjectAccessRequest = "projectAccess.handlebars",
OrgAdminProjectDirectAccess = "orgAdminProjectGrantAccess.handlebars",
OrgAdminBreakglassAccess = "orgAdminBreakglassAccess.handlebars",
ServiceTokenExpired = "serviceTokenExpired.handlebars"
}
@@ -0,0 +1,20 @@
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>Organization admin has bypassed SSO</title>
</head>
<body>
<h2>Infisical</h2>
<p>The organization admin {{email}} has bypassed enforced SSO login.</p>
<p><strong>Timestamp</strong>: {{timestamp}}</p>
<p><strong>IP address</strong>: {{ip}}</p>
<p><strong>User agent</strong>: {{userAgent}}</p>
<p>If you'd like to disable Admin SSO Bypass, please visit <a href="{{siteUrl}}/organization/settings">Organization Settings</a> > Security.</p>
{{emailFooter}}
</body>
</html>