mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 22:27:48 +00:00
Merge branch 'main' of https://github.com/Infisical/infisical into feat/suborg-scope-support
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasCol = await knex.schema.hasColumn(TableName.ScimToken, "expiryNotificationSent");
|
||||
if (!hasCol) {
|
||||
await knex.schema.alterTable(TableName.ScimToken, (t) => {
|
||||
t.boolean("expiryNotificationSent").defaultTo(false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const hasCol = await knex.schema.hasColumn(TableName.ScimToken, "expiryNotificationSent");
|
||||
if (hasCol) {
|
||||
await knex.schema.alterTable(TableName.ScimToken, (t) => {
|
||||
t.dropColumn("expiryNotificationSent");
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,8 @@ export const ScimTokensSchema = z.object({
|
||||
description: z.string(),
|
||||
orgId: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
updatedAt: z.date(),
|
||||
expiryNotificationSent: z.boolean().default(false).nullable().optional()
|
||||
});
|
||||
|
||||
export type TScimTokens = z.infer<typeof ScimTokensSchema>;
|
||||
|
||||
@@ -72,7 +72,6 @@ const ProjectTemplateEnvironmentsSchema = z
|
||||
position: z.number().min(1)
|
||||
})
|
||||
.array()
|
||||
.min(1)
|
||||
.superRefine((environments, ctx) => {
|
||||
if (Buffer.byteLength(JSON.stringify(environments)) > MAX_JSON_SIZE_LIMIT_IN_BYTES)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Size limit exceeded" });
|
||||
@@ -198,7 +197,7 @@ export const registerProjectTemplateRouter = async (server: FastifyZodProvider)
|
||||
description: z.string().max(256).trim().optional().describe(ProjectTemplates.CREATE.description),
|
||||
roles: ProjectTemplateRolesSchema.default([]).describe(ProjectTemplates.CREATE.roles),
|
||||
type: z.nativeEnum(ProjectType).describe(ProjectTemplates.CREATE.type),
|
||||
environments: ProjectTemplateEnvironmentsSchema.describe(ProjectTemplates.CREATE.environments).optional()
|
||||
environments: ProjectTemplateEnvironmentsSchema.nullish().describe(ProjectTemplates.CREATE.environments)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
@@ -243,7 +242,7 @@ export const registerProjectTemplateRouter = async (server: FastifyZodProvider)
|
||||
.describe(ProjectTemplates.UPDATE.name),
|
||||
description: z.string().max(256).trim().optional().describe(ProjectTemplates.UPDATE.description),
|
||||
roles: ProjectTemplateRolesSchema.optional().describe(ProjectTemplates.UPDATE.roles),
|
||||
environments: ProjectTemplateEnvironmentsSchema.optional().describe(ProjectTemplates.UPDATE.environments)
|
||||
environments: ProjectTemplateEnvironmentsSchema.nullish().describe(ProjectTemplates.UPDATE.environments)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
|
||||
@@ -189,11 +189,15 @@ export const projectTemplateServiceFactory = ({
|
||||
message: `A project template with the name "${params.name}" already exists.`
|
||||
});
|
||||
|
||||
const projectTemplateEnvironments =
|
||||
type === ProjectType.SecretManager && environments === undefined
|
||||
? ProjectTemplateDefaultEnvironments
|
||||
: environments;
|
||||
|
||||
const projectTemplate = await projectTemplateDAL.create({
|
||||
...params,
|
||||
roles: JSON.stringify(roles.map((role) => ({ ...role, permissions: packRules(role.permissions) }))),
|
||||
environments:
|
||||
type === ProjectType.SecretManager ? JSON.stringify(environments ?? ProjectTemplateDefaultEnvironments) : null,
|
||||
environments: JSON.stringify(projectTemplateEnvironments),
|
||||
orgId: actor.orgId,
|
||||
type
|
||||
});
|
||||
|
||||
@@ -1,10 +1,56 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { ormify, TOrmify } from "@app/lib/knex";
|
||||
import { AccessScope, OrgMembershipRole, OrgMembershipStatus, TableName } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
export type TScimDALFactory = TOrmify<TableName.ScimToken>;
|
||||
import { TExpiringScimToken } from "./scim-types";
|
||||
|
||||
export const scimDALFactory = (db: TDbClient): TScimDALFactory => {
|
||||
export type TScimDALFactory = ReturnType<typeof scimDALFactory>;
|
||||
|
||||
export const scimDALFactory = (db: TDbClient) => {
|
||||
const scimTokenOrm = ormify(db, TableName.ScimToken);
|
||||
return scimTokenOrm;
|
||||
|
||||
const findExpiringTokens = async (tx?: Knex, batchSize = 500, offset = 0): Promise<TExpiringScimToken[]> => {
|
||||
try {
|
||||
const batch = await (tx || db.replicaNode())(TableName.ScimToken)
|
||||
.leftJoin(TableName.Organization, `${TableName.Organization}.id`, `${TableName.ScimToken}.orgId`)
|
||||
.leftJoin(TableName.Membership, `${TableName.Membership}.scopeOrgId`, `${TableName.ScimToken}.orgId`)
|
||||
.leftJoin(TableName.MembershipRole, `${TableName.MembershipRole}.membershipId`, `${TableName.Membership}.id`)
|
||||
.leftJoin(TableName.Users, `${TableName.Users}.id`, `${TableName.Membership}.actorUserId`)
|
||||
.whereRaw(
|
||||
`
|
||||
(${TableName.ScimToken}."ttlDays" > 0 AND
|
||||
(${TableName.ScimToken}."createdAt" + INTERVAL '1 day' * ${TableName.ScimToken}."ttlDays") < NOW() + INTERVAL '7 days' AND
|
||||
(${TableName.ScimToken}."createdAt" + INTERVAL '1 day' * ${TableName.ScimToken}."ttlDays") > NOW())
|
||||
`
|
||||
)
|
||||
.where(`${TableName.ScimToken}.expiryNotificationSent`, false)
|
||||
.where(`${TableName.Membership}.scope`, AccessScope.Organization)
|
||||
.where(`${TableName.MembershipRole}.role`, OrgMembershipRole.Admin)
|
||||
.whereNot(`${TableName.Membership}.status`, OrgMembershipStatus.Invited)
|
||||
.whereNotNull(`${TableName.Membership}.actorUserId`)
|
||||
.where(`${TableName.Users}.isGhost`, false)
|
||||
.whereNotNull(`${TableName.Users}.email`)
|
||||
.groupBy([`${TableName.ScimToken}.id`, `${TableName.Organization}.name`])
|
||||
.select<TExpiringScimToken[]>([
|
||||
db.ref("id").withSchema(TableName.ScimToken),
|
||||
db.ref("ttlDays").withSchema(TableName.ScimToken),
|
||||
db.ref("description").withSchema(TableName.ScimToken),
|
||||
db.ref("orgId").withSchema(TableName.ScimToken),
|
||||
db.ref("createdAt").withSchema(TableName.ScimToken),
|
||||
db.ref("name").withSchema(TableName.Organization).as("orgName"),
|
||||
db.raw(`array_agg(${TableName.Users}."email") as "adminEmails"`)
|
||||
])
|
||||
.limit(batchSize)
|
||||
.offset(offset);
|
||||
|
||||
return batch;
|
||||
} catch (err) {
|
||||
throw new DatabaseError({ error: err, name: "FindExpiringTokens" });
|
||||
}
|
||||
};
|
||||
|
||||
return { ...scimTokenOrm, findExpiringTokens };
|
||||
};
|
||||
|
||||
@@ -19,6 +19,7 @@ import { TScimDALFactory } from "@app/ee/services/scim/scim-dal";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { crypto } from "@app/lib/crypto";
|
||||
import { BadRequestError, NotFoundError, ScimRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { TAdditionalPrivilegeDALFactory } from "@app/services/additional-privilege/additional-privilege-dal";
|
||||
import { AuthTokenType } from "@app/services/auth/auth-type";
|
||||
@@ -47,7 +48,7 @@ import { buildScimGroup, buildScimGroupList, buildScimUser, buildScimUserList, p
|
||||
import { TScimGroup, TScimServiceFactory } from "./scim-types";
|
||||
|
||||
type TScimServiceFactoryDep = {
|
||||
scimDAL: Pick<TScimDALFactory, "create" | "find" | "findById" | "deleteById">;
|
||||
scimDAL: Pick<TScimDALFactory, "create" | "find" | "findById" | "deleteById" | "findExpiringTokens" | "update">;
|
||||
userDAL: Pick<
|
||||
TUserDALFactory,
|
||||
"find" | "findOne" | "create" | "transaction" | "findUserEncKeyByUserIdsBatch" | "findById" | "updateById"
|
||||
@@ -1237,6 +1238,70 @@ export const scimServiceFactory = ({
|
||||
return { scimTokenId: scimToken.id, orgId: scimToken.orgId };
|
||||
};
|
||||
|
||||
const notifyExpiringTokens: TScimServiceFactory["notifyExpiringTokens"] = async () => {
|
||||
const appCfg = getConfig();
|
||||
let processedCount = 0;
|
||||
let hasMoreRecords = true;
|
||||
let offset = 0;
|
||||
const batchSize = 500;
|
||||
|
||||
while (hasMoreRecords) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const expiringTokens = await scimDAL.findExpiringTokens(undefined, batchSize, offset);
|
||||
|
||||
if (expiringTokens.length === 0) {
|
||||
hasMoreRecords = false;
|
||||
break;
|
||||
}
|
||||
|
||||
const successfullyNotifiedTokenIds: string[] = [];
|
||||
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await Promise.all(
|
||||
expiringTokens.map(async (token) => {
|
||||
try {
|
||||
if (token.adminEmails.length === 0) {
|
||||
// Still mark as notified to avoid repeated checks
|
||||
successfullyNotifiedTokenIds.push(token.id);
|
||||
return;
|
||||
}
|
||||
|
||||
const createdOn = new Date(token.createdAt);
|
||||
const expiringOn = new Date(createdOn.getTime() + Number(token.ttlDays) * 86400 * 1000);
|
||||
|
||||
await smtpService.sendMail({
|
||||
recipients: token.adminEmails,
|
||||
subjectLine: "SCIM Token Expiry Notice",
|
||||
template: SmtpTemplates.ScimTokenExpired,
|
||||
substitutions: {
|
||||
tokenDescription: token.description,
|
||||
orgName: token.orgName,
|
||||
url: `${appCfg.SITE_URL}/organizations/${token.orgId}/settings?selectedTab=provisioning-settings`,
|
||||
createdOn,
|
||||
expiringOn
|
||||
}
|
||||
});
|
||||
|
||||
successfullyNotifiedTokenIds.push(token.id);
|
||||
} catch (error) {
|
||||
logger.error(error, `Failed to send expiration notification for SCIM token ${token.id}:`);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Batch update all successfully notified tokens in a single query
|
||||
if (successfullyNotifiedTokenIds.length > 0) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await scimDAL.update({ $in: { id: successfullyNotifiedTokenIds } }, { expiryNotificationSent: true });
|
||||
}
|
||||
|
||||
processedCount += expiringTokens.length;
|
||||
offset += batchSize;
|
||||
}
|
||||
|
||||
return processedCount;
|
||||
};
|
||||
|
||||
return {
|
||||
createScimToken,
|
||||
listScimTokens,
|
||||
@@ -1253,6 +1318,7 @@ export const scimServiceFactory = ({
|
||||
deleteScimGroup,
|
||||
replaceScimGroup,
|
||||
updateScimGroup,
|
||||
fnValidateScimToken
|
||||
fnValidateScimToken,
|
||||
notifyExpiringTokens
|
||||
};
|
||||
};
|
||||
|
||||
@@ -158,6 +158,16 @@ export type TScimGroup = {
|
||||
};
|
||||
};
|
||||
|
||||
export type TExpiringScimToken = {
|
||||
id: string;
|
||||
ttlDays: number;
|
||||
description: string;
|
||||
orgId: string;
|
||||
createdAt: Date;
|
||||
orgName: string;
|
||||
adminEmails: string[];
|
||||
};
|
||||
|
||||
export type TScimServiceFactory = {
|
||||
createScimToken: (arg: TCreateScimTokenDTO) => Promise<{
|
||||
scimToken: string;
|
||||
@@ -200,4 +210,5 @@ export type TScimServiceFactory = {
|
||||
scimTokenId: string;
|
||||
orgId: string;
|
||||
}>;
|
||||
notifyExpiringTokens: () => Promise<number>;
|
||||
};
|
||||
|
||||
@@ -1329,7 +1329,8 @@ export const registerRoutes = async (
|
||||
eventBusService,
|
||||
licenseService,
|
||||
membershipRoleDAL,
|
||||
membershipUserDAL
|
||||
membershipUserDAL,
|
||||
telemetryService
|
||||
});
|
||||
|
||||
const projectService = projectServiceFactory({
|
||||
@@ -1912,6 +1913,7 @@ export const registerRoutes = async (
|
||||
|
||||
// DAILY
|
||||
const dailyResourceCleanUp = dailyResourceCleanUpQueueServiceFactory({
|
||||
scimService,
|
||||
auditLogDAL,
|
||||
queueService,
|
||||
secretVersionDAL,
|
||||
|
||||
@@ -10,7 +10,12 @@ import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { IntegrationMetadataSchema } from "@app/services/integration/integration-schema";
|
||||
import { Integrations } from "@app/services/integration-auth/integration-list";
|
||||
import { PostHogEventTypes, TIntegrationCreatedEvent } from "@app/services/telemetry/telemetry-types";
|
||||
import {
|
||||
PostHogEventTypes,
|
||||
TIntegrationCreatedEvent,
|
||||
TIntegrationDeletedEvent,
|
||||
TIntegrationSyncedEvent
|
||||
} from "@app/services/telemetry/telemetry-types";
|
||||
|
||||
import {} from "../sanitizedSchemas";
|
||||
|
||||
@@ -288,31 +293,47 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
|
||||
shouldDeleteIntegrationSecrets: req.query.shouldDeleteIntegrationSecrets
|
||||
});
|
||||
|
||||
const deleteIntegrationEventProperty = shake({
|
||||
integrationId: integration.id,
|
||||
integration: integration.integration,
|
||||
environment: integration.environment.slug,
|
||||
secretPath: integration.secretPath,
|
||||
url: integration.url,
|
||||
app: integration.app,
|
||||
appId: integration.appId,
|
||||
targetEnvironment: integration.targetEnvironment,
|
||||
targetEnvironmentId: integration.targetEnvironmentId,
|
||||
targetService: integration.targetService,
|
||||
targetServiceId: integration.targetServiceId,
|
||||
path: integration.path,
|
||||
region: integration.region
|
||||
}) as TIntegrationDeletedEvent["properties"];
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: integration.projectId,
|
||||
event: {
|
||||
type: EventType.DELETE_INTEGRATION,
|
||||
// eslint-disable-next-line
|
||||
metadata: shake({
|
||||
integrationId: integration.id,
|
||||
integration: integration.integration,
|
||||
environment: integration.environment.slug,
|
||||
secretPath: integration.secretPath,
|
||||
url: integration.url,
|
||||
app: integration.app,
|
||||
appId: integration.appId,
|
||||
targetEnvironment: integration.targetEnvironment,
|
||||
targetEnvironmentId: integration.targetEnvironmentId,
|
||||
targetService: integration.targetService,
|
||||
targetServiceId: integration.targetServiceId,
|
||||
path: integration.path,
|
||||
region: integration.region,
|
||||
metadata: {
|
||||
...deleteIntegrationEventProperty,
|
||||
shouldDeleteIntegrationSecrets: req.query.shouldDeleteIntegrationSecrets
|
||||
// eslint-disable-next-line
|
||||
}) as any
|
||||
} as any
|
||||
}
|
||||
});
|
||||
|
||||
await server.services.telemetry.sendPostHogEvents({
|
||||
event: PostHogEventTypes.IntegrationDeleted,
|
||||
organizationId: req.permission.orgId,
|
||||
distinctId: getTelemetryDistinctId(req),
|
||||
properties: {
|
||||
...deleteIntegrationEventProperty,
|
||||
projectId: integration.projectId,
|
||||
...req.auditLogInfo
|
||||
}
|
||||
});
|
||||
|
||||
return { integration };
|
||||
}
|
||||
});
|
||||
@@ -351,28 +372,41 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
|
||||
id: req.params.integrationId
|
||||
});
|
||||
|
||||
const syncIntegrationEventProperty = shake({
|
||||
integrationId: integration.id,
|
||||
integration: integration.integration,
|
||||
environment: integration.environment.slug,
|
||||
secretPath: integration.secretPath,
|
||||
url: integration.url,
|
||||
app: integration.app,
|
||||
appId: integration.appId,
|
||||
targetEnvironment: integration.targetEnvironment,
|
||||
targetEnvironmentId: integration.targetEnvironmentId,
|
||||
targetService: integration.targetService,
|
||||
targetServiceId: integration.targetServiceId,
|
||||
path: integration.path,
|
||||
region: integration.region
|
||||
}) as TIntegrationSyncedEvent["properties"];
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: integration.projectId,
|
||||
event: {
|
||||
type: EventType.MANUAL_SYNC_INTEGRATION,
|
||||
// eslint-disable-next-line
|
||||
metadata: shake({
|
||||
integrationId: integration.id,
|
||||
integration: integration.integration,
|
||||
environment: integration.environment.slug,
|
||||
secretPath: integration.secretPath,
|
||||
url: integration.url,
|
||||
app: integration.app,
|
||||
appId: integration.appId,
|
||||
targetEnvironment: integration.targetEnvironment,
|
||||
targetEnvironmentId: integration.targetEnvironmentId,
|
||||
targetService: integration.targetService,
|
||||
targetServiceId: integration.targetServiceId,
|
||||
path: integration.path,
|
||||
region: integration.region
|
||||
// eslint-disable-next-line
|
||||
}) as any
|
||||
metadata: syncIntegrationEventProperty as any
|
||||
}
|
||||
});
|
||||
|
||||
await server.services.telemetry.sendPostHogEvents({
|
||||
event: PostHogEventTypes.IntegrationSynced,
|
||||
organizationId: req.permission.orgId,
|
||||
distinctId: getTelemetryDistinctId(req),
|
||||
properties: {
|
||||
...syncIntegrationEventProperty,
|
||||
projectId: integration.projectId,
|
||||
isManualSync: true,
|
||||
...req.auditLogInfo
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -123,13 +123,28 @@ export const identityOidcAuthServiceFactory = ({
|
||||
}
|
||||
|
||||
const requestAgent = new https.Agent({ ca: caCert, rejectUnauthorized: !!caCert });
|
||||
const { data: discoveryDoc } = await axios.get<{ jwks_uri: string }>(
|
||||
`${identityOidcAuth.oidcDiscoveryUrl}/.well-known/openid-configuration`,
|
||||
{
|
||||
httpsAgent: identityOidcAuth.oidcDiscoveryUrl.includes("https") ? requestAgent : undefined
|
||||
}
|
||||
);
|
||||
|
||||
let discoveryDoc: { jwks_uri: string };
|
||||
try {
|
||||
const response = await axios.get<{ jwks_uri: string }>(
|
||||
`${identityOidcAuth.oidcDiscoveryUrl}/.well-known/openid-configuration`,
|
||||
{
|
||||
httpsAgent: identityOidcAuth.oidcDiscoveryUrl.includes("https") ? requestAgent : undefined
|
||||
}
|
||||
);
|
||||
discoveryDoc = response.data;
|
||||
} catch (error) {
|
||||
throw new UnauthorizedError({
|
||||
message: `Access denied: Failed to fetch OIDC discovery document from ${identityOidcAuth.oidcDiscoveryUrl}. ${error instanceof Error ? error.message : String(error)}`
|
||||
});
|
||||
}
|
||||
|
||||
const jwksUri = discoveryDoc.jwks_uri;
|
||||
if (!jwksUri) {
|
||||
throw new UnauthorizedError({
|
||||
message: `Access denied: OIDC discovery document does not contain a jwks_uri. The identity provider may be misconfigured.`
|
||||
});
|
||||
}
|
||||
|
||||
const decodedToken = crypto.jwt().decode(oidcJwt, { complete: true });
|
||||
if (!decodedToken) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { TAuditLogDALFactory } from "@app/ee/services/audit-log/audit-log-dal";
|
||||
import { TScimServiceFactory } from "@app/ee/services/scim/scim-types";
|
||||
import { TSnapshotDALFactory } from "@app/ee/services/secret-snapshot/snapshot-dal";
|
||||
import { TKeyValueStoreDALFactory } from "@app/keystore/key-value-store-dal";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
@@ -29,6 +30,7 @@ type TDailyResourceCleanUpQueueServiceFactoryDep = {
|
||||
orgService: TOrgServiceFactory;
|
||||
userNotificationDAL: Pick<TUserNotificationDALFactory, "pruneNotifications">;
|
||||
keyValueStoreDAL: Pick<TKeyValueStoreDALFactory, "pruneExpiredKeys">;
|
||||
scimService: Pick<TScimServiceFactory, "notifyExpiringTokens">;
|
||||
};
|
||||
|
||||
export type TDailyResourceCleanUpQueueServiceFactory = ReturnType<typeof dailyResourceCleanUpQueueServiceFactory>;
|
||||
@@ -44,6 +46,7 @@ export const dailyResourceCleanUpQueueServiceFactory = ({
|
||||
secretVersionV2DAL,
|
||||
identityUniversalAuthClientSecretDAL,
|
||||
serviceTokenService,
|
||||
scimService,
|
||||
orgService,
|
||||
userNotificationDAL,
|
||||
keyValueStoreDAL
|
||||
@@ -86,6 +89,7 @@ export const dailyResourceCleanUpQueueServiceFactory = ({
|
||||
await secretVersionV2DAL.pruneExcessVersions();
|
||||
await secretFolderVersionDAL.pruneExcessVersions();
|
||||
await serviceTokenService.notifyExpiringTokens();
|
||||
await scimService.notifyExpiringTokens();
|
||||
await orgService.notifyInvitedUsers();
|
||||
await auditLogDAL.pruneAuditLog();
|
||||
await userNotificationDAL.pruneNotifications();
|
||||
|
||||
@@ -421,11 +421,12 @@ export const fnSecretBulkDelete = async ({
|
||||
);
|
||||
|
||||
const changes = deletedSecrets
|
||||
.filter(({ type }) => type === SecretType.Shared)
|
||||
.filter(({ type, id }) => type === SecretType.Shared && secretVersions[id])
|
||||
.map(({ id }) => ({
|
||||
type: CommitType.DELETE,
|
||||
secretVersionId: secretVersions[id].id
|
||||
secretVersionId: secretVersions[id]?.id
|
||||
}));
|
||||
|
||||
if (changes.length > 0) {
|
||||
if (commitChanges) {
|
||||
commitChanges.push(...changes);
|
||||
|
||||
@@ -2254,7 +2254,8 @@ export const secretV2BridgeServiceFactory = ({
|
||||
]
|
||||
}
|
||||
});
|
||||
if (secretsToDelete.length !== inputSecrets.length)
|
||||
const secretsToDeleteSet = new Set(secretsToDelete.map((el) => el.key));
|
||||
if (secretsToDeleteSet.size !== inputSecrets.length)
|
||||
throw new NotFoundError({
|
||||
message: `One or more secrets does not exist: ${secretsToDelete.map((el) => el.key).join(", ")}`
|
||||
});
|
||||
|
||||
@@ -64,6 +64,8 @@ import { expandSecretReferencesFactory, getAllSecretReferences } from "../secret
|
||||
import { TSecretVersionV2DALFactory } from "../secret-v2-bridge/secret-version-dal";
|
||||
import { TSecretVersionV2TagDALFactory } from "../secret-v2-bridge/secret-version-tag-dal";
|
||||
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
||||
import { TTelemetryServiceFactory } from "../telemetry/telemetry-service";
|
||||
import { PostHogEventTypes } from "../telemetry/telemetry-types";
|
||||
import { TUserDALFactory } from "../user/user-dal";
|
||||
import { TWebhookDALFactory } from "../webhook/webhook-dal";
|
||||
import { fnTriggerWebhook } from "../webhook/webhook-fns";
|
||||
@@ -120,6 +122,7 @@ type TSecretQueueFactoryDep = {
|
||||
reminderService: Pick<TReminderServiceFactory, "createReminderInternal" | "deleteReminderBySecretId">;
|
||||
eventBusService: TEventBusService;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
telemetryService: Pick<TTelemetryServiceFactory, "sendPostHogEvents">;
|
||||
};
|
||||
|
||||
export type TGetSecrets = {
|
||||
@@ -184,7 +187,8 @@ export const secretQueueFactory = ({
|
||||
eventBusService,
|
||||
licenseService,
|
||||
membershipUserDAL,
|
||||
membershipRoleDAL
|
||||
membershipRoleDAL,
|
||||
telemetryService
|
||||
}: TSecretQueueFactoryDep) => {
|
||||
const integrationMeter = opentelemetry.metrics.getMeter("Integrations");
|
||||
const errorHistogram = integrationMeter.createHistogram("integration_secret_sync_errors", {
|
||||
@@ -1029,6 +1033,29 @@ export const secretQueueFactory = ({
|
||||
isSynced: response?.isSynced ?? true
|
||||
});
|
||||
|
||||
await telemetryService.sendPostHogEvents({
|
||||
event: PostHogEventTypes.IntegrationSynced,
|
||||
distinctId: `project/${projectId}`,
|
||||
organizationId: project.orgId,
|
||||
properties: {
|
||||
integrationId: integration.id,
|
||||
integration: integration.integration,
|
||||
environment,
|
||||
secretPath,
|
||||
projectId,
|
||||
url: integration.url ?? undefined,
|
||||
app: integration.app ?? undefined,
|
||||
appId: integration.appId ?? undefined,
|
||||
targetEnvironment: integration.targetEnvironment ?? undefined,
|
||||
targetEnvironmentId: integration.targetEnvironmentId ?? undefined,
|
||||
targetService: integration.targetService ?? undefined,
|
||||
targetServiceId: integration.targetServiceId ?? undefined,
|
||||
path: integration.path ?? undefined,
|
||||
region: integration.region ?? undefined,
|
||||
isManualSync: isManual ?? false
|
||||
}
|
||||
});
|
||||
|
||||
// May be undefined, if it's undefined we assume the sync was successful, hence the strict equality type check.
|
||||
if (response?.isSynced === false) {
|
||||
integrationsFailedToSync.push({
|
||||
|
||||
@@ -214,6 +214,8 @@ export const serviceTokenServiceFactory = ({
|
||||
break;
|
||||
}
|
||||
|
||||
const successfullyNotifiedTokenIds: string[] = [];
|
||||
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await Promise.all(
|
||||
expiringTokens.map(async (token) => {
|
||||
@@ -228,13 +230,19 @@ export const serviceTokenServiceFactory = ({
|
||||
url: `${appCfg.SITE_URL}/organizations/${token.orgId}/projects/secret-management/${token.projectId}/access-management?selectedTab=service-tokens`
|
||||
}
|
||||
});
|
||||
await serviceTokenDAL.update({ id: token.id }, { expiryNotificationSent: true });
|
||||
successfullyNotifiedTokenIds.push(token.id);
|
||||
} catch (error) {
|
||||
logger.error(error, `Failed to send expiration notification for token ${token.id}:`);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Batch update all successfully notified tokens in a single query
|
||||
if (successfullyNotifiedTokenIds.length > 0) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await serviceTokenDAL.update({ $in: { id: successfullyNotifiedTokenIds } }, { expiryNotificationSent: true });
|
||||
}
|
||||
|
||||
processedCount += expiringTokens.length;
|
||||
offset += batchSize;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import { Heading, Section, Text } from "@react-email/components";
|
||||
import React from "react";
|
||||
|
||||
import { BaseButton } from "./BaseButton";
|
||||
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
|
||||
|
||||
interface ScimTokenExpiryNoticeTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
|
||||
tokenDescription?: string;
|
||||
orgName: string;
|
||||
createdOn: Date;
|
||||
expiringOn: Date;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export const ScimTokenExpiryNoticeTemplate = ({
|
||||
tokenDescription,
|
||||
siteUrl,
|
||||
orgName,
|
||||
url,
|
||||
createdOn,
|
||||
expiringOn
|
||||
}: ScimTokenExpiryNoticeTemplateProps) => {
|
||||
const formatDate = (date: Date) =>
|
||||
date.toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric"
|
||||
});
|
||||
|
||||
const createdOnDisplay = formatDate(createdOn);
|
||||
const expiringOnDisplay = formatDate(expiringOn);
|
||||
|
||||
return (
|
||||
<BaseEmailWrapper title="SCIM Token Expiring Soon" preview="A SCIM token is about to expire." siteUrl={siteUrl}>
|
||||
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
|
||||
<strong>SCIM token expiry notice</strong>
|
||||
</Heading>
|
||||
<Section className="px-[24px] mb-[28px] mt-[36px] pt-[12px] pb-[8px] border border-solid border-gray-200 rounded-md bg-gray-50">
|
||||
<Text className="text-[14px]">
|
||||
{tokenDescription ? (
|
||||
<>
|
||||
Your SCIM token <strong>{tokenDescription}</strong>
|
||||
</>
|
||||
) : (
|
||||
"One of your SCIM tokens"
|
||||
)}{" "}
|
||||
for <strong>{orgName}</strong>, created on <strong>{createdOnDisplay}</strong>, is scheduled to expire on{" "}
|
||||
<strong>{expiringOnDisplay}</strong>.
|
||||
</Text>
|
||||
<Text>
|
||||
If this token is still needed for your external platform sync, please create a new one before it expires to
|
||||
avoid disruption to your workflow.
|
||||
</Text>
|
||||
</Section>
|
||||
<Section className="text-center">
|
||||
<BaseButton href={url}>Manage SCIM Tokens</BaseButton>
|
||||
</Section>
|
||||
</BaseEmailWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default ScimTokenExpiryNoticeTemplate;
|
||||
|
||||
ScimTokenExpiryNoticeTemplate.PreviewProps = {
|
||||
orgName: "Example Organization",
|
||||
siteUrl: "https://infisical.com",
|
||||
url: "https://infisical.com",
|
||||
tokenDescription: "Example SCIM Token",
|
||||
createdOn: new Date("2025-11-27T00:00:00Z"),
|
||||
expiringOn: new Date("2025-12-27T00:00:00Z")
|
||||
} as ScimTokenExpiryNoticeTemplateProps;
|
||||
@@ -19,6 +19,7 @@ export * from "./PasswordSetupTemplate";
|
||||
export * from "./PkiExpirationAlertTemplate";
|
||||
export * from "./ProjectAccessRequestTemplate";
|
||||
export * from "./ProjectInvitationTemplate";
|
||||
export * from "./ScimTokenExpiryNoticeTemplate";
|
||||
export * from "./ScimUserProvisionedTemplate";
|
||||
export * from "./SecretApprovalRequestBypassedTemplate";
|
||||
export * from "./SecretApprovalRequestNeedsReviewTemplate";
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
PkiExpirationAlertTemplate,
|
||||
ProjectAccessRequestTemplate,
|
||||
ProjectInvitationTemplate,
|
||||
ScimTokenExpiryNoticeTemplate,
|
||||
ScimUserProvisionedTemplate,
|
||||
SecretApprovalRequestBypassedTemplate,
|
||||
SecretApprovalRequestNeedsReviewTemplate,
|
||||
@@ -75,6 +76,7 @@ export enum SmtpTemplates {
|
||||
SecretLeakIncident = "secretLeakIncident",
|
||||
WorkspaceInvite = "workspaceInvitation",
|
||||
ScimUserProvisioned = "scimUserProvisioned",
|
||||
ScimTokenExpired = "scimTokenExpired",
|
||||
PkiExpirationAlert = "pkiExpirationAlert",
|
||||
IntegrationSyncFailed = "integrationSyncFailed",
|
||||
SecretSyncFailed = "secretSyncFailed",
|
||||
@@ -123,6 +125,7 @@ const EmailTemplateMap: Record<SmtpTemplates, React.FC<any>> = {
|
||||
[SmtpTemplates.SecretLeakIncident]: SecretLeakIncidentTemplate,
|
||||
[SmtpTemplates.WorkspaceInvite]: ProjectInvitationTemplate,
|
||||
[SmtpTemplates.ScimUserProvisioned]: ScimUserProvisionedTemplate,
|
||||
[SmtpTemplates.ScimTokenExpired]: ScimTokenExpiryNoticeTemplate,
|
||||
[SmtpTemplates.SecretRequestCompleted]: SecretRequestCompletedTemplate,
|
||||
[SmtpTemplates.UnlockAccount]: UnlockAccountTemplate,
|
||||
[SmtpTemplates.ServiceTokenExpired]: ServiceTokenExpiryNoticeTemplate,
|
||||
|
||||
@@ -21,6 +21,8 @@ export enum PostHogEventTypes {
|
||||
SecretScannerPush = "cloud secret scan",
|
||||
ProjectCreated = "Project Created",
|
||||
IntegrationCreated = "Integration Created",
|
||||
IntegrationSynced = "Integration Synced",
|
||||
IntegrationDeleted = "Integration Deleted",
|
||||
MachineIdentityCreated = "Machine Identity Created",
|
||||
UserOrgInvitation = "User Org Invitation",
|
||||
TelemetryInstanceStats = "Self Hosted Instance Stats",
|
||||
@@ -126,6 +128,47 @@ export type TIntegrationCreatedEvent = {
|
||||
};
|
||||
};
|
||||
|
||||
export type TIntegrationSyncedEvent = {
|
||||
event: PostHogEventTypes.IntegrationSynced;
|
||||
properties: {
|
||||
projectId: string;
|
||||
integrationId: string;
|
||||
integration: string;
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
isManualSync: boolean;
|
||||
url?: string;
|
||||
app?: string;
|
||||
appId?: string;
|
||||
targetEnvironment?: string;
|
||||
targetEnvironmentId?: string;
|
||||
targetService?: string;
|
||||
targetServiceId?: string;
|
||||
path?: string;
|
||||
region?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type TIntegrationDeletedEvent = {
|
||||
event: PostHogEventTypes.IntegrationDeleted;
|
||||
properties: {
|
||||
projectId: string;
|
||||
integrationId: string;
|
||||
integration: string;
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
url?: string;
|
||||
app?: string;
|
||||
appId?: string;
|
||||
targetEnvironment?: string;
|
||||
targetEnvironmentId?: string;
|
||||
targetService?: string;
|
||||
targetServiceId?: string;
|
||||
path?: string;
|
||||
region?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type TUserOrgInvitedEvent = {
|
||||
event: PostHogEventTypes.UserOrgInvitation;
|
||||
properties: {
|
||||
@@ -249,6 +292,8 @@ export type TPostHogEvent = { distinctId: string; organizationId?: string } & (
|
||||
| TUserOrgInvitedEvent
|
||||
| TMachineIdentityCreatedEvent
|
||||
| TIntegrationCreatedEvent
|
||||
| TIntegrationSyncedEvent
|
||||
| TIntegrationDeletedEvent
|
||||
| TProjectCreateEvent
|
||||
| TTelemetryInstanceStatsEvent
|
||||
| TSecretRequestCreatedEvent
|
||||
|
||||
Reference in New Issue
Block a user