mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 23:27:14 +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",
|
GET_PROJECT_KMS_BACKUP = "get-project-kms-backup",
|
||||||
LOAD_PROJECT_KMS_BACKUP = "load-project-kms-backup",
|
LOAD_PROJECT_KMS_BACKUP = "load-project-kms-backup",
|
||||||
ORG_ADMIN_ACCESS_PROJECT = "org-admin-accessed-project",
|
ORG_ADMIN_ACCESS_PROJECT = "org-admin-accessed-project",
|
||||||
|
ORG_ADMIN_BYPASS_SSO = "org-admin-bypassed-sso",
|
||||||
CREATE_CERTIFICATE_TEMPLATE = "create-certificate-template",
|
CREATE_CERTIFICATE_TEMPLATE = "create-certificate-template",
|
||||||
UPDATE_CERTIFICATE_TEMPLATE = "update-certificate-template",
|
UPDATE_CERTIFICATE_TEMPLATE = "update-certificate-template",
|
||||||
DELETE_CERTIFICATE_TEMPLATE = "delete-certificate-template",
|
DELETE_CERTIFICATE_TEMPLATE = "delete-certificate-template",
|
||||||
@@ -1907,6 +1908,11 @@ interface OrgAdminAccessProjectEvent {
|
|||||||
}; // no metadata yet
|
}; // no metadata yet
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface OrgAdminBypassSSOEvent {
|
||||||
|
type: EventType.ORG_ADMIN_BYPASS_SSO;
|
||||||
|
metadata: Record<string, string>; // no metadata yet
|
||||||
|
}
|
||||||
|
|
||||||
interface CreateCertificateTemplateEstConfig {
|
interface CreateCertificateTemplateEstConfig {
|
||||||
type: EventType.CREATE_CERTIFICATE_TEMPLATE_EST_CONFIG;
|
type: EventType.CREATE_CERTIFICATE_TEMPLATE_EST_CONFIG;
|
||||||
metadata: {
|
metadata: {
|
||||||
@@ -2656,6 +2662,7 @@ export type Event =
|
|||||||
| GetProjectKmsBackupEvent
|
| GetProjectKmsBackupEvent
|
||||||
| LoadProjectKmsBackupEvent
|
| LoadProjectKmsBackupEvent
|
||||||
| OrgAdminAccessProjectEvent
|
| OrgAdminAccessProjectEvent
|
||||||
|
| OrgAdminBypassSSOEvent
|
||||||
| CreateCertificateTemplate
|
| CreateCertificateTemplate
|
||||||
| UpdateCertificateTemplate
|
| UpdateCertificateTemplate
|
||||||
| GetCertificateTemplate
|
| GetCertificateTemplate
|
||||||
|
|||||||
@@ -25,11 +25,11 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
|||||||
customRateLimits: false,
|
customRateLimits: false,
|
||||||
customAlerts: false,
|
customAlerts: false,
|
||||||
secretAccessInsights: false,
|
secretAccessInsights: false,
|
||||||
auditLogs: false,
|
auditLogs: true,
|
||||||
auditLogsRetentionDays: 0,
|
auditLogsRetentionDays: 3,
|
||||||
auditLogStreams: false,
|
auditLogStreams: false,
|
||||||
auditLogStreamLimit: 3,
|
auditLogStreamLimit: 3,
|
||||||
samlSSO: false,
|
samlSSO: true,
|
||||||
hsm: false,
|
hsm: false,
|
||||||
oidcSSO: false,
|
oidcSSO: false,
|
||||||
scim: false,
|
scim: false,
|
||||||
|
|||||||
@@ -596,7 +596,14 @@ export const registerRoutes = async (
|
|||||||
kmsService
|
kmsService
|
||||||
});
|
});
|
||||||
|
|
||||||
const loginService = authLoginServiceFactory({ userDAL, smtpService, tokenService, orgDAL, totpService });
|
const loginService = authLoginServiceFactory({
|
||||||
|
userDAL,
|
||||||
|
smtpService,
|
||||||
|
tokenService,
|
||||||
|
orgDAL,
|
||||||
|
totpService,
|
||||||
|
auditLogService
|
||||||
|
});
|
||||||
const passwordService = authPaswordServiceFactory({
|
const passwordService = authPaswordServiceFactory({
|
||||||
tokenService,
|
tokenService,
|
||||||
smtpService,
|
smtpService,
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import jwt from "jsonwebtoken";
|
|||||||
import { Knex } from "knex";
|
import { Knex } from "knex";
|
||||||
|
|
||||||
import { OrgMembershipRole, TUsers, UserDeviceSchema } from "@app/db/schemas";
|
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 { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { request } from "@app/lib/config/request";
|
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 { getUserPrivateKey } from "@app/lib/crypto/srp";
|
||||||
import { BadRequestError, DatabaseError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors";
|
import { BadRequestError, DatabaseError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||||
import { logger } from "@app/lib/logger";
|
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 { getServerCfg } from "@app/services/super-admin/super-admin-service";
|
||||||
|
|
||||||
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
|
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
|
||||||
@@ -28,7 +31,14 @@ import {
|
|||||||
TOauthTokenExchangeDTO,
|
TOauthTokenExchangeDTO,
|
||||||
TVerifyMfaTokenDTO
|
TVerifyMfaTokenDTO
|
||||||
} from "./auth-login-type";
|
} 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 = {
|
type TAuthLoginServiceFactoryDep = {
|
||||||
userDAL: TUserDALFactory;
|
userDAL: TUserDALFactory;
|
||||||
@@ -36,6 +46,7 @@ type TAuthLoginServiceFactoryDep = {
|
|||||||
tokenService: TAuthTokenServiceFactory;
|
tokenService: TAuthTokenServiceFactory;
|
||||||
smtpService: TSmtpService;
|
smtpService: TSmtpService;
|
||||||
totpService: Pick<TTotpServiceFactory, "verifyUserTotp" | "verifyWithUserRecoveryCode">;
|
totpService: Pick<TTotpServiceFactory, "verifyUserTotp" | "verifyWithUserRecoveryCode">;
|
||||||
|
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TAuthLoginFactory = ReturnType<typeof authLoginServiceFactory>;
|
export type TAuthLoginFactory = ReturnType<typeof authLoginServiceFactory>;
|
||||||
@@ -44,7 +55,8 @@ export const authLoginServiceFactory = ({
|
|||||||
tokenService,
|
tokenService,
|
||||||
smtpService,
|
smtpService,
|
||||||
orgDAL,
|
orgDAL,
|
||||||
totpService
|
totpService,
|
||||||
|
auditLogService
|
||||||
}: TAuthLoginServiceFactoryDep) => {
|
}: TAuthLoginServiceFactoryDep) => {
|
||||||
/*
|
/*
|
||||||
* Private
|
* Private
|
||||||
@@ -412,6 +424,51 @@ export const authLoginServiceFactory = ({
|
|||||||
mfaMethod: decodedToken.mfaMethod
|
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 {
|
return {
|
||||||
...tokens,
|
...tokens,
|
||||||
isMfaEnabled: false
|
isMfaEnabled: false
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Knex } from "knex";
|
|||||||
|
|
||||||
import { TDbClient } from "@app/db";
|
import { TDbClient } from "@app/db";
|
||||||
import {
|
import {
|
||||||
|
OrgMembershipRole,
|
||||||
TableName,
|
TableName,
|
||||||
TOrganizations,
|
TOrganizations,
|
||||||
TOrganizationsInsert,
|
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) => {
|
const findOrgGhostUser = async (orgId: string) => {
|
||||||
try {
|
try {
|
||||||
const member = await db
|
const member = await db
|
||||||
@@ -472,6 +510,7 @@ export const orgDALFactory = (db: TDbClient) => {
|
|||||||
findAllOrgsByUserId,
|
findAllOrgsByUserId,
|
||||||
ghostUserExists,
|
ghostUserExists,
|
||||||
findOrgMembersByUsername,
|
findOrgMembersByUsername,
|
||||||
|
findOrgMembersByRole,
|
||||||
findOrgGhostUser,
|
findOrgGhostUser,
|
||||||
create,
|
create,
|
||||||
updateById,
|
updateById,
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ export enum SmtpTemplates {
|
|||||||
SecretRotationFailed = "secretRotationFailed.handlebars",
|
SecretRotationFailed = "secretRotationFailed.handlebars",
|
||||||
ProjectAccessRequest = "projectAccess.handlebars",
|
ProjectAccessRequest = "projectAccess.handlebars",
|
||||||
OrgAdminProjectDirectAccess = "orgAdminProjectGrantAccess.handlebars",
|
OrgAdminProjectDirectAccess = "orgAdminProjectGrantAccess.handlebars",
|
||||||
|
OrgAdminBreakglassAccess = "OrgAdminBreakglassAccess.handlebars",
|
||||||
ServiceTokenExpired = "serviceTokenExpired.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",
|
ADD_PKI_COLLECTION_ITEM = "add-pki-collection-item",
|
||||||
DELETE_PKI_COLLECTION_ITEM = "delete-pki-collection-item",
|
DELETE_PKI_COLLECTION_ITEM = "delete-pki-collection-item",
|
||||||
ORG_ADMIN_ACCESS_PROJECT = "org-admin-accessed-project",
|
ORG_ADMIN_ACCESS_PROJECT = "org-admin-accessed-project",
|
||||||
|
ORG_ADMIN_BYPASS_SSO = "org-admin-bypassed-sso",
|
||||||
CREATE_CERTIFICATE_TEMPLATE = "create-certificate-template",
|
CREATE_CERTIFICATE_TEMPLATE = "create-certificate-template",
|
||||||
UPDATE_CERTIFICATE_TEMPLATE = "update-certificate-template",
|
UPDATE_CERTIFICATE_TEMPLATE = "update-certificate-template",
|
||||||
DELETE_CERTIFICATE_TEMPLATE = "delete-certificate-template",
|
DELETE_CERTIFICATE_TEMPLATE = "delete-certificate-template",
|
||||||
|
|||||||
@@ -718,6 +718,11 @@ interface OrgAdminAccessProjectEvent {
|
|||||||
}; // no metadata yet
|
}; // no metadata yet
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface OrgAdminBypassSSOEvent {
|
||||||
|
type: EventType.ORG_ADMIN_BYPASS_SSO;
|
||||||
|
metadata: Record<string, string>; // no metadata yet
|
||||||
|
}
|
||||||
|
|
||||||
interface CreateCertificateTemplate {
|
interface CreateCertificateTemplate {
|
||||||
type: EventType.CREATE_CERTIFICATE_TEMPLATE;
|
type: EventType.CREATE_CERTIFICATE_TEMPLATE;
|
||||||
metadata: {
|
metadata: {
|
||||||
@@ -885,6 +890,7 @@ export type Event =
|
|||||||
| AddPkiCollectionItem
|
| AddPkiCollectionItem
|
||||||
| DeletePkiCollectionItem
|
| DeletePkiCollectionItem
|
||||||
| OrgAdminAccessProjectEvent
|
| OrgAdminAccessProjectEvent
|
||||||
|
| OrgAdminBypassSSOEvent
|
||||||
| CreateCertificateTemplate
|
| CreateCertificateTemplate
|
||||||
| UpdateCertificateTemplate
|
| UpdateCertificateTemplate
|
||||||
| GetCertificateTemplate
|
| GetCertificateTemplate
|
||||||
|
|||||||
Reference in New Issue
Block a user