mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Admin SSO bypass (breakglass login) sends out email to all org admins + creates audit log
This commit is contained in:
@@ -234,6 +234,7 @@ export enum EventType {
|
||||
GET_PROJECT_KMS_BACKUP = "get-project-kms-backup",
|
||||
LOAD_PROJECT_KMS_BACKUP = "load-project-kms-backup",
|
||||
ORG_ADMIN_ACCESS_PROJECT = "org-admin-accessed-project",
|
||||
ORG_ADMIN_BYPASS_SSO = "org-admin-bypassed-sso",
|
||||
CREATE_CERTIFICATE_TEMPLATE = "create-certificate-template",
|
||||
UPDATE_CERTIFICATE_TEMPLATE = "update-certificate-template",
|
||||
DELETE_CERTIFICATE_TEMPLATE = "delete-certificate-template",
|
||||
@@ -1907,6 +1908,11 @@ interface OrgAdminAccessProjectEvent {
|
||||
}; // no metadata yet
|
||||
}
|
||||
|
||||
interface OrgAdminBypassSSOEvent {
|
||||
type: EventType.ORG_ADMIN_BYPASS_SSO;
|
||||
metadata: Record<string, string>; // no metadata yet
|
||||
}
|
||||
|
||||
interface CreateCertificateTemplateEstConfig {
|
||||
type: EventType.CREATE_CERTIFICATE_TEMPLATE_EST_CONFIG;
|
||||
metadata: {
|
||||
@@ -2656,6 +2662,7 @@ export type Event =
|
||||
| GetProjectKmsBackupEvent
|
||||
| LoadProjectKmsBackupEvent
|
||||
| OrgAdminAccessProjectEvent
|
||||
| OrgAdminBypassSSOEvent
|
||||
| CreateCertificateTemplate
|
||||
| UpdateCertificateTemplate
|
||||
| GetCertificateTemplate
|
||||
|
||||
@@ -25,11 +25,11 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
||||
customRateLimits: false,
|
||||
customAlerts: false,
|
||||
secretAccessInsights: false,
|
||||
auditLogs: false,
|
||||
auditLogsRetentionDays: 0,
|
||||
auditLogs: true,
|
||||
auditLogsRetentionDays: 3,
|
||||
auditLogStreams: false,
|
||||
auditLogStreamLimit: 3,
|
||||
samlSSO: false,
|
||||
samlSSO: true,
|
||||
hsm: false,
|
||||
oidcSSO: false,
|
||||
scim: false,
|
||||
|
||||
@@ -596,7 +596,14 @@ export const registerRoutes = async (
|
||||
kmsService
|
||||
});
|
||||
|
||||
const loginService = authLoginServiceFactory({ userDAL, smtpService, tokenService, orgDAL, totpService });
|
||||
const loginService = authLoginServiceFactory({
|
||||
userDAL,
|
||||
smtpService,
|
||||
tokenService,
|
||||
orgDAL,
|
||||
totpService,
|
||||
auditLogService
|
||||
});
|
||||
const passwordService = authPaswordServiceFactory({
|
||||
tokenService,
|
||||
smtpService,
|
||||
|
||||
@@ -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,14 @@ 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";
|
||||
|
||||
type TAuthLoginServiceFactoryDep = {
|
||||
userDAL: TUserDALFactory;
|
||||
@@ -36,6 +46,7 @@ type TAuthLoginServiceFactoryDep = {
|
||||
tokenService: TAuthTokenServiceFactory;
|
||||
smtpService: TSmtpService;
|
||||
totpService: Pick<TTotpServiceFactory, "verifyUserTotp" | "verifyWithUserRecoveryCode">;
|
||||
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
|
||||
};
|
||||
|
||||
export type TAuthLoginFactory = ReturnType<typeof authLoginServiceFactory>;
|
||||
@@ -44,7 +55,8 @@ export const authLoginServiceFactory = ({
|
||||
tokenService,
|
||||
smtpService,
|
||||
orgDAL,
|
||||
totpService
|
||||
totpService,
|
||||
auditLogService
|
||||
}: TAuthLoginServiceFactoryDep) => {
|
||||
/*
|
||||
* Private
|
||||
@@ -412,6 +424,51 @@ 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
|
||||
const orgAdmins = await orgDAL.findOrgMembersByRole(organizationId, OrgMembershipRole.Admin);
|
||||
const adminEmails = orgAdmins.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
|
||||
},
|
||||
template: SmtpTemplates.OrgAdminBreakglassAccess
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...tokens,
|
||||
isMfaEnabled: false
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import {
|
||||
OrgMembershipRole,
|
||||
TableName,
|
||||
TOrganizations,
|
||||
TOrganizationsInsert,
|
||||
@@ -251,6 +252,43 @@ export const orgDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
const findOrgMembersByRole = async (orgId: string, role: OrgMembershipRole, tx?: Knex) => {
|
||||
try {
|
||||
const conn = tx || db;
|
||||
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 +510,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 just 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 Organization Settings > Security.</p>
|
||||
|
||||
{{emailFooter}}
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -90,6 +90,7 @@ export enum EventType {
|
||||
ADD_PKI_COLLECTION_ITEM = "add-pki-collection-item",
|
||||
DELETE_PKI_COLLECTION_ITEM = "delete-pki-collection-item",
|
||||
ORG_ADMIN_ACCESS_PROJECT = "org-admin-accessed-project",
|
||||
ORG_ADMIN_BYPASS_SSO = "org-admin-bypassed-sso",
|
||||
CREATE_CERTIFICATE_TEMPLATE = "create-certificate-template",
|
||||
UPDATE_CERTIFICATE_TEMPLATE = "update-certificate-template",
|
||||
DELETE_CERTIFICATE_TEMPLATE = "delete-certificate-template",
|
||||
|
||||
@@ -718,6 +718,11 @@ interface OrgAdminAccessProjectEvent {
|
||||
}; // no metadata yet
|
||||
}
|
||||
|
||||
interface OrgAdminBypassSSOEvent {
|
||||
type: EventType.ORG_ADMIN_BYPASS_SSO;
|
||||
metadata: Record<string, string>; // no metadata yet
|
||||
}
|
||||
|
||||
interface CreateCertificateTemplate {
|
||||
type: EventType.CREATE_CERTIFICATE_TEMPLATE;
|
||||
metadata: {
|
||||
@@ -885,6 +890,7 @@ export type Event =
|
||||
| AddPkiCollectionItem
|
||||
| DeletePkiCollectionItem
|
||||
| OrgAdminAccessProjectEvent
|
||||
| OrgAdminBypassSSOEvent
|
||||
| CreateCertificateTemplate
|
||||
| UpdateCertificateTemplate
|
||||
| GetCertificateTemplate
|
||||
|
||||
Reference in New Issue
Block a user