Compare commits

..

24 Commits

Author SHA1 Message Date
fd792e7e1d misc: finalized error codes for oidc login 2024-09-19 15:00:52 +08:00
5740d2b4e4 Merge pull request #2429 from Infisical/daniel/integration-ui-improvements
feat: integration details page with logging
2024-09-17 14:29:26 +04:00
09887a7405 Update ConfiguredIntegrationItem.tsx 2024-09-16 23:05:38 +04:00
38ee3a005e Requested changes 2024-09-16 22:26:36 +04:00
10e7999334 Merge pull request #2439 from Infisical/misc/address-slack-env-related-error
misc: addressed slack env config validation error
2024-09-17 02:16:07 +08:00
8c458588ab misc: removed from .env.example 2024-09-17 01:25:16 +08:00
2381a2e4ba misc: addressed slack env config validation error 2024-09-17 01:19:45 +08:00
74653e7ed1 Minor ui improvements 2024-09-16 13:56:23 +04:00
8a0b1bb427 Update IntegrationAuditLogsSection.tsx 2024-09-15 20:34:08 +04:00
1f6faadf81 Cleanup 2024-09-15 20:24:23 +04:00
8f3b7e1698 feat: audit logs event metadata & remapping support 2024-09-15 20:01:43 +04:00
24c460c695 feat: integration details page 2024-09-15 20:00:43 +04:00
8acceab1e7 fix: updated last used to be considered last success sync 2024-09-15 19:57:56 +04:00
d60aba9339 fix: added missing integration metadata attributes 2024-09-15 19:57:36 +04:00
3a228f7521 feat: improved audit logs 2024-09-15 19:57:02 +04:00
3f7ac0f142 feat: integration synced log event 2024-09-15 19:52:43 +04:00
63cf535ebb feat: platform-level actor for logs 2024-09-15 19:52:13 +04:00
69a2a46c47 Update organization-router.ts 2024-09-15 19:51:54 +04:00
d081077273 feat: integration sync logs 2024-09-15 19:51:38 +04:00
75034f9350 feat: more expendable audit logs 2024-09-15 19:50:03 +04:00
eacd7b0c6a feat: made audit logs more searchable with better filters 2024-09-15 19:49:35 +04:00
5bad77083c feat: more expendable audit logs 2024-09-15 19:49:07 +04:00
1025759efb Feat: Integration Audit Logs 2024-09-13 21:00:47 +04:00
5e5ab29ab9 Feat: Integration UI improvements 2024-09-12 13:09:00 +04:00
50 changed files with 1328 additions and 211 deletions

View File

@ -72,6 +72,3 @@ PLAIN_API_KEY=
PLAIN_WISH_LABEL_IDS=
SSL_CLIENT_CERTIFICATE_HEADER_KEY=
WORKFLOW_SLACK_CLIENT_ID=
WORKFLOW_SLACK_CLIENT_SECRET=

View File

@ -146,12 +146,16 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
actorId: req.permission.id,
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
projectId: req.params.workspaceId,
...req.query,
endDate: req.query.endDate,
startDate: req.query.startDate || getLastMidnightDateISO(),
auditLogActor: req.query.actor,
actor: req.permission.type
actor: req.permission.type,
filter: {
...req.query,
projectId: req.params.workspaceId,
endDate: req.query.endDate,
startDate: req.query.startDate || getLastMidnightDateISO(),
auditLogActorId: req.query.actor,
eventType: req.query.eventType ? [req.query.eventType] : undefined
}
});
return { auditLogs };
}

View File

@ -6,6 +6,9 @@ import { DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols, stripUndefinedInWhere } from "@app/lib/knex";
import { logger } from "@app/lib/logger";
import { QueueName } from "@app/queue";
import { ActorType } from "@app/services/auth/auth-type";
import { EventType } from "./audit-log-types";
export type TAuditLogDALFactory = ReturnType<typeof auditLogDALFactory>;
@ -25,7 +28,24 @@ export const auditLogDALFactory = (db: TDbClient) => {
const auditLogOrm = ormify(db, TableName.AuditLog);
const find = async (
{ orgId, projectId, userAgentType, startDate, endDate, limit = 20, offset = 0, actor, eventType }: TFindQuery,
{
orgId,
projectId,
userAgentType,
startDate,
endDate,
limit = 20,
offset = 0,
actorId,
actorType,
eventType,
eventMetadata
}: Omit<TFindQuery, "actor" | "eventType"> & {
actorId?: string;
actorType?: ActorType;
eventType?: EventType[];
eventMetadata?: Record<string, string>;
},
tx?: Knex
) => {
try {
@ -34,7 +54,6 @@ export const auditLogDALFactory = (db: TDbClient) => {
stripUndefinedInWhere({
projectId,
[`${TableName.AuditLog}.orgId`]: orgId,
eventType,
userAgentType
})
)
@ -52,8 +71,22 @@ export const auditLogDALFactory = (db: TDbClient) => {
.offset(offset)
.orderBy(`${TableName.AuditLog}.createdAt`, "desc");
if (actor) {
void sqlQuery.whereRaw(`"actorMetadata"->>'userId' = ?`, [actor]);
if (actorId) {
void sqlQuery.whereRaw(`"actorMetadata"->>'userId' = ?`, [actorId]);
}
if (eventMetadata && Object.keys(eventMetadata).length) {
Object.entries(eventMetadata).forEach(([key, value]) => {
void sqlQuery.whereRaw(`"eventMetadata"->>'${key}' = ?`, [value]);
});
}
if (actorType) {
void sqlQuery.where("actor", actorType);
}
if (eventType?.length) {
void sqlQuery.whereIn("eventType", eventType);
}
if (startDate) {

View File

@ -23,25 +23,12 @@ export const auditLogServiceFactory = ({
auditLogQueue,
permissionService
}: TAuditLogServiceFactoryDep) => {
const listAuditLogs = async ({
userAgentType,
eventType,
offset,
limit,
endDate,
startDate,
actor,
actorId,
actorOrgId,
actorAuthMethod,
projectId,
auditLogActor
}: TListProjectAuditLogDTO) => {
if (projectId) {
const listAuditLogs = async ({ actorAuthMethod, actorId, actorOrgId, actor, filter }: TListProjectAuditLogDTO) => {
if (filter.projectId) {
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
filter.projectId,
actorAuthMethod,
actorOrgId
);
@ -65,14 +52,16 @@ export const auditLogServiceFactory = ({
// If project ID is not provided, then we need to return all the audit logs for the organization itself.
const auditLogs = await auditLogDAL.find({
startDate,
endDate,
limit,
offset,
eventType,
userAgentType,
actor: auditLogActor,
...(projectId ? { projectId } : { orgId: actorOrgId })
startDate: filter.startDate,
endDate: filter.endDate,
limit: filter.limit,
offset: filter.offset,
eventType: filter.eventType,
userAgentType: filter.userAgentType,
actorId: filter.auditLogActorId,
actorType: filter.actorType,
eventMetadata: filter.eventMetadata,
...(filter.projectId ? { projectId: filter.projectId } : { orgId: actorOrgId })
});
return auditLogs.map(({ eventType: logEventType, actor: eActor, actorMetadata, eventMetadata, ...el }) => ({

View File

@ -5,19 +5,23 @@ import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
import { PkiItemType } from "@app/services/pki-collection/pki-collection-types";
export type TListProjectAuditLogDTO = {
auditLogActor?: string;
projectId?: string;
eventType?: string;
startDate?: string;
endDate?: string;
userAgentType?: string;
limit?: number;
offset?: number;
filter: {
userAgentType?: UserAgentType;
eventType?: EventType[];
offset?: number;
limit: number;
endDate?: string;
startDate?: string;
projectId?: string;
auditLogActorId?: string;
actorType?: ActorType;
eventMetadata?: Record<string, string>;
};
} & Omit<TProjectPermission, "projectId">;
export type TCreateAuditLogDTO = {
event: Event;
actor: UserActor | IdentityActor | ServiceActor | ScimClientActor;
actor: UserActor | IdentityActor | ServiceActor | ScimClientActor | PlatformActor;
orgId?: string;
projectId?: string;
} & BaseAuthData;
@ -177,7 +181,8 @@ export enum EventType {
UPDATE_SLACK_INTEGRATION = "update-slack-integration",
DELETE_SLACK_INTEGRATION = "delete-slack-integration",
GET_PROJECT_SLACK_CONFIG = "get-project-slack-config",
UPDATE_PROJECT_SLACK_CONFIG = "update-project-slack-config"
UPDATE_PROJECT_SLACK_CONFIG = "update-project-slack-config",
INTEGRATION_SYNCED = "integration-synced"
}
interface UserActorMetadata {
@ -198,6 +203,8 @@ interface IdentityActorMetadata {
interface ScimClientActorMetadata {}
interface PlatformActorMetadata {}
export interface UserActor {
type: ActorType.USER;
metadata: UserActorMetadata;
@ -208,6 +215,11 @@ export interface ServiceActor {
metadata: ServiceActorMetadata;
}
export interface PlatformActor {
type: ActorType.PLATFORM;
metadata: PlatformActorMetadata;
}
export interface IdentityActor {
type: ActorType.IDENTITY;
metadata: IdentityActorMetadata;
@ -218,7 +230,7 @@ export interface ScimClientActor {
metadata: ScimClientActorMetadata;
}
export type Actor = UserActor | ServiceActor | IdentityActor | ScimClientActor;
export type Actor = UserActor | ServiceActor | IdentityActor | ScimClientActor | PlatformActor;
interface GetSecretsEvent {
type: EventType.GET_SECRETS;
@ -1518,6 +1530,16 @@ interface GetProjectSlackConfig {
id: string;
};
}
interface IntegrationSyncedEvent {
type: EventType.INTEGRATION_SYNCED;
metadata: {
integrationId: string;
lastSyncJobId: string;
lastUsed: Date;
syncMessage: string;
isSynced: boolean;
};
}
export type Event =
| GetSecretsEvent
@ -1657,4 +1679,5 @@ export type Event =
| DeleteSlackIntegration
| GetSlackIntegration
| UpdateProjectSlackConfig
| GetProjectSlackConfig;
| GetProjectSlackConfig
| IntegrationSyncedEvent;

View File

@ -147,8 +147,8 @@ const envSchema = z
PLAIN_WISH_LABEL_IDS: zpStr(z.string().optional()),
DISABLE_AUDIT_LOG_GENERATION: zodStrBool.default("false"),
SSL_CLIENT_CERTIFICATE_HEADER_KEY: zpStr(z.string().optional()).default("x-ssl-client-cert"),
WORKFLOW_SLACK_CLIENT_ID: zpStr(z.string()).optional(),
WORKFLOW_SLACK_CLIENT_SECRET: zpStr(z.string()).optional()
WORKFLOW_SLACK_CLIENT_ID: zpStr(z.string().optional()),
WORKFLOW_SLACK_CLIENT_SECRET: zpStr(z.string().optional())
})
.transform((data) => ({
...data,

View File

@ -91,6 +91,8 @@ export type TQueueJobTypes = {
[QueueName.IntegrationSync]: {
name: QueueJobs.IntegrationSync;
payload: {
isManual?: boolean;
actorId?: string;
projectId: string;
environment: string;
secretPath: string;

View File

@ -810,6 +810,8 @@ export const registerRoutes = async (
projectEnvDAL,
webhookDAL,
orgDAL,
auditLogService,
userDAL,
projectMembershipDAL,
smtpService,
projectDAL,

View File

@ -22,7 +22,7 @@ export const registerIdentityAwsAuthRouter = async (server: FastifyZodProvider)
schema: {
description: "Login with AWS Auth",
body: z.object({
identityId: z.string().trim().describe(AWS_AUTH.LOGIN.identityId),
identityId: z.string().describe(AWS_AUTH.LOGIN.identityId),
iamHttpRequestMethod: z.string().default("POST").describe(AWS_AUTH.LOGIN.iamHttpRequestMethod),
iamRequestBody: z.string().describe(AWS_AUTH.LOGIN.iamRequestBody),
iamRequestHeaders: z.string().describe(AWS_AUTH.LOGIN.iamRequestHeaders)

View File

@ -19,7 +19,7 @@ export const registerIdentityAzureAuthRouter = async (server: FastifyZodProvider
schema: {
description: "Login with Azure Auth",
body: z.object({
identityId: z.string().trim().describe(AZURE_AUTH.LOGIN.identityId),
identityId: z.string().describe(AZURE_AUTH.LOGIN.identityId),
jwt: z.string()
}),
response: {

View File

@ -19,7 +19,7 @@ export const registerIdentityGcpAuthRouter = async (server: FastifyZodProvider)
schema: {
description: "Login with GCP Auth",
body: z.object({
identityId: z.string().trim().describe(GCP_AUTH.LOGIN.identityId).trim(),
identityId: z.string().describe(GCP_AUTH.LOGIN.identityId),
jwt: z.string()
}),
response: {

View File

@ -4,7 +4,7 @@ import { IntegrationsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { INTEGRATION } from "@app/lib/api-docs";
import { removeTrailingSlash, shake } from "@app/lib/fn";
import { writeLimit } from "@app/server/config/rateLimiter";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
@ -154,6 +154,48 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
}
});
server.route({
method: "GET",
url: "/:integrationId",
config: {
rateLimit: readLimit
},
schema: {
description: "Get an integration by integration id",
security: [
{
bearerAuth: []
}
],
params: z.object({
integrationId: z.string().trim().describe(INTEGRATION.UPDATE.integrationId)
}),
response: {
200: z.object({
integration: IntegrationsSchema.extend({
environment: z.object({
slug: z.string().trim(),
name: z.string().trim(),
id: z.string().trim()
})
})
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const integration = await server.services.integration.getIntegration({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.params.integrationId
});
return { integration };
}
});
server.route({
method: "DELETE",
url: "/:integrationId",

View File

@ -14,7 +14,7 @@ import { AUDIT_LOGS, ORGANIZATIONS } from "@app/lib/api-docs";
import { getLastMidnightDateISO } from "@app/lib/fn";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { ActorType, AuthMode } from "@app/services/auth/auth-type";
export const registerOrgRouter = async (server: FastifyZodProvider) => {
server.route({
@ -74,8 +74,35 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
schema: {
description: "Get all audit logs for an organization",
querystring: z.object({
eventType: z.nativeEnum(EventType).optional().describe(AUDIT_LOGS.EXPORT.eventType),
projectId: z.string().optional(),
actorType: z.nativeEnum(ActorType).optional(),
// eventType is split with , for multiple values, we need to transform it to array
eventType: z
.string()
.optional()
.transform((val) => (val ? val.split(",") : undefined)),
userAgentType: z.nativeEnum(UserAgentType).optional().describe(AUDIT_LOGS.EXPORT.userAgentType),
eventMetadata: z
.string()
.optional()
.transform((val) => {
if (!val) {
return undefined;
}
const pairs = val.split(",");
return pairs.reduce(
(acc, pair) => {
const [key, value] = pair.split("=");
if (key && value) {
acc[key] = value;
}
return acc;
},
{} as Record<string, string>
);
}),
startDate: z.string().datetime().optional().describe(AUDIT_LOGS.EXPORT.startDate),
endDate: z.string().datetime().optional().describe(AUDIT_LOGS.EXPORT.endDate),
offset: z.coerce.number().default(0).describe(AUDIT_LOGS.EXPORT.offset),
@ -114,13 +141,19 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const auditLogs = await server.services.auditLog.listAuditLogs({
filter: {
...req.query,
endDate: req.query.endDate,
projectId: req.query.projectId,
startDate: req.query.startDate || getLastMidnightDateISO(),
auditLogActorId: req.query.actor,
actorType: req.query.actorType,
eventType: req.query.eventType as EventType[] | undefined
},
actorId: req.permission.id,
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
...req.query,
endDate: req.query.endDate,
startDate: req.query.startDate || getLastMidnightDateISO(),
auditLogActor: req.query.actor,
actor: req.permission.type
});
return { auditLogs };

View File

@ -34,6 +34,7 @@ export enum AuthMode {
}
export enum ActorType { // would extend to AWS, Azure, ...
PLATFORM = "platform", // Useful for when we want to perform logging on automated actions such as integration syncs.
USER = "user", // userIdentity
SERVICE = "service",
IDENTITY = "identity",

View File

@ -18,7 +18,7 @@ import {
infisicalSymmetricDecrypt,
infisicalSymmetricEncypt
} from "@app/lib/crypto/encryption";
import { BadRequestError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors";
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
import { ActorType, AuthTokenType } from "../auth/auth-type";
@ -68,12 +68,12 @@ export const identityOidcAuthServiceFactory = ({
identityId: identityOidcAuth.identityId
});
if (!identityMembershipOrg) {
throw new BadRequestError({ message: "Failed to find identity" });
throw new NotFoundError({ message: "Failed to find identity in organization" });
}
const orgBot = await orgBotDAL.findOne({ orgId: identityMembershipOrg.orgId });
if (!orgBot) {
throw new BadRequestError({ message: "Org bot not found", name: "OrgBotNotFound" });
throw new NotFoundError({ message: "Org bot not found", name: "OrgBotNotFound" });
}
const key = infisicalSymmetricDecrypt({
@ -106,7 +106,7 @@ export const identityOidcAuthServiceFactory = ({
const decodedToken = jwt.decode(oidcJwt, { complete: true });
if (!decodedToken) {
throw new BadRequestError({
throw new UnauthorizedError({
message: "Invalid JWT"
});
}
@ -119,13 +119,24 @@ export const identityOidcAuthServiceFactory = ({
const { kid } = decodedToken.header;
const oidcSigningKey = await client.getSigningKey(kid);
const tokenData = jwt.verify(oidcJwt, oidcSigningKey.getPublicKey(), {
issuer: identityOidcAuth.boundIssuer
}) as Record<string, string>;
let tokenData: Record<string, string>;
try {
tokenData = jwt.verify(oidcJwt, oidcSigningKey.getPublicKey(), {
issuer: identityOidcAuth.boundIssuer
}) as Record<string, string>;
} catch (error) {
if (error instanceof jwt.JsonWebTokenError) {
throw new UnauthorizedError({
message: `Access denied: ${error.message}`
});
}
throw error;
}
if (identityOidcAuth.boundSubject) {
if (!doesFieldValueMatchOidcPolicy(tokenData.sub, identityOidcAuth.boundSubject)) {
throw new ForbiddenRequestError({
throw new UnauthorizedError({
message: "Access denied: OIDC subject not allowed."
});
}
@ -137,7 +148,7 @@ export const identityOidcAuthServiceFactory = ({
.split(", ")
.some((policyValue) => doesFieldValueMatchOidcPolicy(tokenData.aud, policyValue))
) {
throw new ForbiddenRequestError({
throw new UnauthorizedError({
message: "Access denied: OIDC audience not allowed."
});
}
@ -150,7 +161,7 @@ export const identityOidcAuthServiceFactory = ({
if (
!claimValue.split(", ").some((claimEntry) => doesFieldValueMatchOidcPolicy(tokenData[claimKey], claimEntry))
) {
throw new ForbiddenRequestError({
throw new UnauthorizedError({
message: "Access denied: OIDC claim not allowed."
});
}

View File

@ -2,7 +2,7 @@ import { ForbiddenError, subject } from "@casl/ability";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { BadRequestError } from "@app/lib/errors";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { TProjectPermission } from "@app/lib/types";
import { TIntegrationAuthDALFactory } from "../integration-auth/integration-auth-dal";
@ -19,6 +19,7 @@ import { TIntegrationDALFactory } from "./integration-dal";
import {
TCreateIntegrationDTO,
TDeleteIntegrationDTO,
TGetIntegrationDTO,
TSyncIntegrationDTO,
TUpdateIntegrationDTO
} from "./integration-types";
@ -180,6 +181,27 @@ export const integrationServiceFactory = ({
return updatedIntegration;
};
const getIntegration = async ({ id, actor, actorAuthMethod, actorId, actorOrgId }: TGetIntegrationDTO) => {
const integration = await integrationDAL.findById(id);
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
integration?.projectId || "",
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
if (!integration) {
throw new NotFoundError({
message: "Integration not found"
});
}
return { ...integration, envId: integration.environment.id };
};
const deleteIntegration = async ({
actorId,
id,
@ -276,6 +298,8 @@ export const integrationServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
await secretQueueService.syncIntegrations({
isManual: true,
actorId,
environment: integration.environment.slug,
secretPath: integration.secretPath,
projectId: integration.projectId
@ -289,6 +313,7 @@ export const integrationServiceFactory = ({
updateIntegration,
deleteIntegration,
listIntegrationByProject,
getIntegration,
syncIntegration
};
};

View File

@ -39,6 +39,10 @@ export type TCreateIntegrationDTO = {
};
} & Omit<TProjectPermission, "projectId">;
export type TGetIntegrationDTO = {
id: string;
} & Omit<TProjectPermission, "projectId">;
export type TUpdateIntegrationDTO = {
id: string;
app?: string;

View File

@ -2,6 +2,8 @@
import { AxiosError } from "axios";
import { ProjectUpgradeStatus, ProjectVersion, TSecretSnapshotSecretsV2, TSecretVersionsV2 } from "@app/db/schemas";
import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service";
import { Actor, EventType } from "@app/ee/services/audit-log/audit-log-types";
import { TSecretApprovalRequestDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-dal";
import { TSecretRotationDALFactory } from "@app/ee/services/secret-rotation/secret-rotation-dal";
import { TSnapshotDALFactory } from "@app/ee/services/secret-snapshot/snapshot-dal";
@ -21,6 +23,7 @@ import { TSecretVersionTagDALFactory } from "@app/services/secret/secret-version
import { TSecretBlindIndexDALFactory } from "@app/services/secret-blind-index/secret-blind-index-dal";
import { TSecretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal";
import { ActorType } from "../auth/auth-type";
import { TIntegrationDALFactory } from "../integration/integration-dal";
import { TIntegrationAuthDALFactory } from "../integration-auth/integration-auth-dal";
import { TIntegrationAuthServiceFactory } from "../integration-auth/integration-auth-service";
@ -40,6 +43,7 @@ import { expandSecretReferencesFactory, getAllNestedSecretReferences } from "../
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 { TUserDALFactory } from "../user/user-dal";
import { TWebhookDALFactory } from "../webhook/webhook-dal";
import { fnTriggerWebhook } from "../webhook/webhook-fns";
import { TSecretDALFactory } from "./secret-dal";
@ -71,6 +75,7 @@ type TSecretQueueFactoryDep = {
secretVersionDAL: TSecretVersionDALFactory;
secretBlindIndexDAL: TSecretBlindIndexDALFactory;
secretTagDAL: TSecretTagDALFactory;
userDAL: Pick<TUserDALFactory, "findById">;
secretVersionTagDAL: TSecretVersionTagDALFactory;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
secretV2BridgeDAL: TSecretV2BridgeDALFactory;
@ -81,6 +86,7 @@ type TSecretQueueFactoryDep = {
snapshotDAL: Pick<TSnapshotDALFactory, "findNSecretV1SnapshotByFolderId" | "deleteSnapshotsAboveLimit">;
snapshotSecretV2BridgeDAL: Pick<TSnapshotSecretV2DALFactory, "insertMany" | "batchInsert">;
keyStore: Pick<TKeyStoreFactory, "acquireLock" | "setItemWithExpiry" | "getItem">;
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
};
export type TGetSecrets = {
@ -106,6 +112,7 @@ export const secretQueueFactory = ({
secretDAL,
secretImportDAL,
folderDAL,
userDAL,
webhookDAL,
projectEnvDAL,
orgDAL,
@ -125,7 +132,8 @@ export const secretQueueFactory = ({
snapshotDAL,
snapshotSecretV2BridgeDAL,
secretApprovalRequestDAL,
keyStore
keyStore,
auditLogService
}: TSecretQueueFactoryDep) => {
const removeSecretReminder = async (dto: TRemoveSecretReminderDTO) => {
const appCfg = getConfig();
@ -430,7 +438,9 @@ export const secretQueueFactory = ({
return content;
};
const syncIntegrations = async (dto: TGetSecrets & { deDupeQueue?: Record<string, boolean> }) => {
const syncIntegrations = async (
dto: TGetSecrets & { isManual?: boolean; actorId?: string; deDupeQueue?: Record<string, boolean> }
) => {
await queueService.queue(QueueName.IntegrationSync, QueueJobs.IntegrationSync, dto, {
attempts: 3,
delay: 1000,
@ -528,7 +538,7 @@ export const secretQueueFactory = ({
}
}
);
await syncIntegrations({ secretPath, projectId, environment, deDupeQueue });
await syncIntegrations({ secretPath, projectId, environment, deDupeQueue, isManual: false });
if (!excludeReplication) {
await replicateSecrets({
_deDupeReplicationQueue: deDupeReplicationQueue,
@ -544,7 +554,7 @@ export const secretQueueFactory = ({
});
queueService.start(QueueName.IntegrationSync, async (job) => {
const { environment, projectId, secretPath, depth = 1, deDupeQueue = {} } = job.data;
const { environment, actorId, isManual, projectId, secretPath, depth = 1, deDupeQueue = {} } = job.data;
if (depth > MAX_SYNC_SECRET_DEPTH) return;
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
@ -693,6 +703,30 @@ export const secretQueueFactory = ({
});
}
const generateActor = async (): Promise<Actor> => {
if (isManual && actorId) {
const user = await userDAL.findById(actorId);
if (!user) {
throw new Error("User not found");
}
return {
type: ActorType.USER,
metadata: {
email: user.email,
username: user.username,
userId: user.id
}
};
}
return {
type: ActorType.PLATFORM,
metadata: {}
};
};
// akhilmhdh: this try catch is for lock release
try {
const secrets = shouldUseSecretV2Bridge
@ -778,6 +812,21 @@ export const secretQueueFactory = ({
}
});
await auditLogService.createAuditLog({
projectId,
actor: await generateActor(),
event: {
type: EventType.INTEGRATION_SYNCED,
metadata: {
integrationId: integration.id,
isSynced: response?.isSynced ?? true,
lastSyncJobId: job?.id ?? "",
lastUsed: new Date(),
syncMessage: response?.syncMessage ?? ""
}
}
});
await integrationDAL.updateById(integration.id, {
lastSyncJobId: job.id,
lastUsed: new Date(),
@ -794,9 +843,23 @@ export const secretQueueFactory = ({
(err instanceof AxiosError ? JSON.stringify(err?.response?.data) : (err as Error)?.message) ||
"Unknown error occurred.";
await auditLogService.createAuditLog({
projectId,
actor: await generateActor(),
event: {
type: EventType.INTEGRATION_SYNCED,
metadata: {
integrationId: integration.id,
isSynced: false,
lastSyncJobId: job?.id ?? "",
lastUsed: new Date(),
syncMessage: message
}
}
});
await integrationDAL.updateById(integration.id, {
lastSyncJobId: job.id,
lastUsed: new Date(),
syncMessage: message,
isSynced: false
});

View File

@ -51,6 +51,7 @@ infisical export --template=<path to template>
<Info>
Alternatively, you may use service tokens.
Please note, however, that service tokens are being deprecated in favor of [machine identities](/documentation/platform/identities/machine-identities). They will be removed in the future in accordance with the deprecation notice and timeline stated [here](https://infisical.com/blog/deprecating-api-keys).
```bash
# Example
export INFISICAL_TOKEN=<service-token>

View File

@ -54,6 +54,8 @@ $ infisical run -- npm run dev
<Info>
Alternatively, you may use service tokens.
Please note, however, that service tokens are being deprecated in favor of [machine identities](/documentation/platform/identities/machine-identities). They will be removed in the future in accordance with the deprecation notice and timeline stated [here](https://infisical.com/blog/deprecating-api-keys).
```bash
# Example
export INFISICAL_TOKEN=<service-token>

View File

@ -33,6 +33,7 @@ $ infisical secrets
<Info>
Alternatively, you may use service tokens.
Please note, however, that service tokens are being deprecated in favor of [machine identities](/documentation/platform/identities/machine-identities). They will be removed in the future in accordance with the deprecation notice and timeline stated [here](https://infisical.com/blog/deprecating-api-keys).
```bash
# Example
export INFISICAL_TOKEN=<service-token>

View File

@ -206,6 +206,8 @@ infisical <any-command> --domain="https://your-self-hosted-infisical.com/api"
</Accordion>
<Accordion title="Can I use the CLI with service tokens?">
Yes. Please note, however, that service tokens are being deprecated in favor of [machine identities](/documentation/platform/identities/machine-identities). They will be removed in the future in accordance with the deprecation notice and timeline stated [here](https://infisical.com/blog/deprecating-api-keys).
To use Infisical for non local development scenarios, please create a service token. The service token will allow you to authenticate and interact with Infisical. Once you have created a service token with the required permissions, youll need to feed the token to the CLI.
```bash

View File

@ -3,6 +3,13 @@ title: "Service Token"
description: "Infisical service tokens allow users to programmatically interact with Infisical."
---
<Warning>
Service tokens are being deprecated in favor of [machine identities](/documentation/platform/identities/machine-identities).
They will be removed in the future in accordance with the deprecation notice and timeline stated [here](https://infisical.com/blog/deprecating-api-keys).
</Warning>
Service tokens are authentication credentials that services can use to access designated endpoints in the Infisical API to manage project resources like secrets.
Each service token can be provisioned scoped access to select environment(s) and path(s) within them.

View File

@ -138,9 +138,16 @@ Prerequisites:
</Tab>
<Tab title="Using CLI with Service Tokens">
<Tab title="Using CLI with Service Tokens (Deprecated)">
## Add Infisical Service Token to Jenkins
<Warning>
Service tokens are being deprecated in favor of [machine identities](/documentation/platform/identities/machine-identities).
They will be removed in the future in accordance with the deprecation notice and timeline stated [here](https://infisical.com/blog/deprecating-api-keys).
**Please use our Jenkins Plugin instead!**
</Warning>
After setting up your project in Infisical and installing the Infisical CLI to the environment where your Jenkins builds will run, you will need to add the Infisical Service Token to Jenkins.
To generate a Infisical service token, follow the guide [here](/documentation/platform/token).

View File

@ -62,6 +62,12 @@ This approach enables you to fetch secrets from Infisical during Amplify build t
</Tab>
<Tab title="Service Token (Deprecated)">
<Warning>
Service tokens are being deprecated in favor of [machine identities](/documentation/platform/identities/machine-identities).
They will be removed in the future in accordance with the deprecation notice and timeline stated [here](https://infisical.com/blog/deprecating-api-keys).
</Warning>
<Steps>
<Step title="Generate a service token">

View File

@ -63,7 +63,14 @@ Follow this [guide](./docker) to configure the Infisical CLI for each service th
```
</Tab>
<Tab title="Service Token">
<Tab title="Service Token (Deprecated)">
<Warning>
Service tokens are being deprecated in favor of [machine identities](/documentation/platform/identities/machine-identities).
They will be removed in the future in accordance with the deprecation notice and timeline stated [here](https://infisical.com/blog/deprecating-api-keys).
</Warning>
## Generate service token
Generate a unique [Service Token](/documentation/platform/token) for each service.

View File

@ -83,6 +83,12 @@ CMD ["infisical", "run", "--projectId", "<your-project-id>", "--command", "npm r
</Tab>
<Tab title="Service Token (Deprecated)">
<Warning>
Service tokens are being deprecated in favor of [machine identities](/documentation/platform/identities/machine-identities).
They will be removed in the future in accordance with the deprecation notice and timeline stated [here](https://infisical.com/blog/deprecating-api-keys).
</Warning>
```dockerfile
CMD ["infisical", "run", "--", "[your service start command]"]

View File

@ -587,6 +587,12 @@ spec:
</Accordion>
<Accordion title="authentication.serviceToken">
<Warning>
Service tokens are being deprecated in favor of [machine identities](/documentation/platform/identities/machine-identities).
They will be removed in the future in accordance with the deprecation notice and timeline stated [here](https://infisical.com/blog/deprecating-api-keys).
</Warning>
The service token required to authenticate with Infisical needs to be stored in a Kubernetes secret. This block defines the reference to the name and namespace of secret that stores this service token.
Follow the instructions below to create and store the service token in a Kubernetes secrets and reference it in your CRD.

View File

@ -2,6 +2,13 @@
title: "Service tokens"
description: "Understanding service tokens and their best practices."
---
<Warning>
Service tokens are being deprecated in favor of [machine identities](/documentation/platform/identities/machine-identities).
They will be removed in the future in accordance with the deprecation notice and timeline stated [here](https://infisical.com/blog/deprecating-api-keys).
</Warning>
Many clients use service tokens to authenticate and read/write secrets from/to Infisical; they can be created in your project settings.

View File

@ -79,7 +79,8 @@ export const eventToNameMap: { [K in EventType]: string } = {
[EventType.UPDATE_CERTIFICATE_TEMPLATE_EST_CONFIG]:
"Update certificate template EST configuration",
[EventType.UPDATE_PROJECT_SLACK_CONFIG]: "Update project slack configuration",
[EventType.GET_PROJECT_SLACK_CONFIG]: "Get project slack configuration"
[EventType.GET_PROJECT_SLACK_CONFIG]: "Get project slack configuration",
[EventType.INTEGRATION_SYNCED]: "Integration sync"
};
export const userAgentTTypeoNameMap: { [K in UserAgentType]: string } = {

View File

@ -1,4 +1,5 @@
export enum ActorType {
PLATFORM = "platform",
USER = "user",
SERVICE = "service",
IDENTITY = "identity"
@ -91,5 +92,6 @@ export enum EventType {
UPDATE_CERTIFICATE_TEMPLATE_EST_CONFIG = "update-certificate-template-est-config",
GET_CERTIFICATE_TEMPLATE_EST_CONFIG = "get-certificate-template-est-config",
UPDATE_PROJECT_SLACK_CONFIG = "update-project-slack-config",
GET_PROJECT_SLACK_CONFIG = "get-project-slack-config"
GET_PROJECT_SLACK_CONFIG = "get-project-slack-config",
INTEGRATION_SYNCED = "integration-synced"
}

View File

@ -1,35 +1,58 @@
import { useInfiniteQuery, useQuery } from "@tanstack/react-query";
import { useInfiniteQuery, UseInfiniteQueryOptions, useQuery } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { Actor, AuditLog, AuditLogFilters } from "./types";
import { Actor, AuditLog, TGetAuditLogsFilter } from "./types";
export const auditLogKeys = {
getAuditLogs: (workspaceId: string | null, filters: AuditLogFilters) =>
getAuditLogs: (workspaceId: string | null, filters: TGetAuditLogsFilter) =>
[{ workspaceId, filters }, "audit-logs"] as const,
getAuditLogActorFilterOpts: (workspaceId: string) =>
[{ workspaceId }, "audit-log-actor-filters"] as const
};
export const useGetAuditLogs = (filters: AuditLogFilters, workspaceId: string | null) => {
export const useGetAuditLogs = (
filters: TGetAuditLogsFilter,
projectId: string | null,
options: Omit<
UseInfiniteQueryOptions<
AuditLog[],
unknown,
AuditLog[],
AuditLog[],
ReturnType<typeof auditLogKeys.getAuditLogs>
>,
"queryFn" | "queryKey" | "getNextPageParam"
> = {}
) => {
return useInfiniteQuery({
queryKey: auditLogKeys.getAuditLogs(workspaceId, filters),
queryKey: auditLogKeys.getAuditLogs(projectId, filters),
queryFn: async ({ pageParam }) => {
const auditLogEndpoint = workspaceId
? `/api/v1/workspace/${workspaceId}/audit-logs`
: "/api/v1/organization/audit-logs";
const { data } = await apiRequest.get<{ auditLogs: AuditLog[] }>(auditLogEndpoint, {
params: {
...filters,
offset: pageParam,
startDate: filters?.startDate?.toISOString(),
endDate: filters?.endDate?.toISOString()
const { data } = await apiRequest.get<{ auditLogs: AuditLog[] }>(
"/api/v1/organization/audit-logs",
{
params: {
...filters,
offset: pageParam,
startDate: filters?.startDate?.toISOString(),
endDate: filters?.endDate?.toISOString(),
...(filters.eventMetadata && Object.keys(filters.eventMetadata).length
? {
eventMetadata: Object.entries(filters.eventMetadata)
.map(([key, value]) => `${key}=${value}`)
.join(",")
}
: {}),
...(filters.eventType?.length ? { eventType: filters.eventType.join(",") } : {}),
...(projectId ? { projectId } : {})
}
}
});
);
return data.auditLogs;
},
getNextPageParam: (lastPage, pages) =>
lastPage.length !== 0 ? pages.length * filters.limit : undefined
lastPage.length !== 0 ? pages.length * filters.limit : undefined,
...options
});
};

View File

@ -3,6 +3,17 @@ import { IdentityTrustedIp } from "../identities/types";
import { PkiItemType } from "../pkiCollections/constants";
import { ActorType, EventType, UserAgentType } from "./enums";
export type TGetAuditLogsFilter = {
eventType?: EventType[];
userAgentType?: UserAgentType;
eventMetadata?: Record<string, string>;
actorType?: ActorType;
actorId?: string; // user ID format
startDate?: Date;
endDate?: Date;
limit: number;
};
interface UserActorMetadata {
userId: string;
email: string;
@ -33,7 +44,13 @@ export interface IdentityActor {
metadata: IdentityActorMetadata;
}
export type Actor = UserActor | ServiceActor | IdentityActor;
export interface PlatformActorMetadata {}
export interface PlatformActor {
type: ActorType.PLATFORM;
metadata: PlatformActorMetadata;
}
export type Actor = UserActor | ServiceActor | IdentityActor | PlatformActor;
interface GetSecretsEvent {
type: EventType.GET_SECRETS;
@ -761,6 +778,22 @@ interface GetProjectSlackConfig {
};
}
export enum IntegrationSyncedEventTrigger {
MANUAL = "manual",
AUTO = "auto"
}
interface IntegrationSyncedEvent {
type: EventType.INTEGRATION_SYNCED;
metadata: {
integrationId: string;
lastSyncJobId: string;
lastUsed: Date;
syncMessage: string;
isSynced: boolean;
};
}
export type Event =
| GetSecretsEvent
| GetSecretEvent
@ -838,7 +871,8 @@ export type Event =
| CreateCertificateTemplateEstConfig
| GetCertificateTemplateEstConfig
| UpdateProjectSlackConfig
| GetProjectSlackConfig;
| GetProjectSlackConfig
| IntegrationSyncedEvent;
export type AuditLog = {
id: string;
@ -856,12 +890,3 @@ export type AuditLog = {
slug: string;
};
};
export type AuditLogFilters = {
eventType?: EventType;
userAgentType?: UserAgentType;
actor?: string;
limit: number;
startDate?: Date;
endDate?: Date;
};

View File

@ -1 +1,6 @@
export { useCreateIntegration, useDeleteIntegration, useGetCloudIntegrations } from "./queries";
export {
useCreateIntegration,
useDeleteIntegration,
useGetCloudIntegrations,
useGetIntegration
} from "./queries";

View File

@ -1,13 +1,14 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useMutation, useQuery, useQueryClient, UseQueryOptions } from "@tanstack/react-query";
import { createNotification } from "@app/components/notifications";
import { apiRequest } from "@app/config/request";
import { workspaceKeys } from "../workspace";
import { TCloudIntegration } from "./types";
import { TCloudIntegration, TIntegrationWithEnv } from "./types";
export const integrationQueryKeys = {
getIntegrations: () => ["integrations"] as const
getIntegrations: () => ["integrations"] as const,
getIntegration: (id: string) => ["integration", id] as const
};
const fetchIntegrations = async () => {
@ -18,6 +19,14 @@ const fetchIntegrations = async () => {
return data.integrationOptions;
};
const fetchIntegration = async (id: string) => {
const { data } = await apiRequest.get<{ integration: TIntegrationWithEnv }>(
`/api/v1/integration/${id}`
);
return data.integration;
};
export const useGetCloudIntegrations = () =>
useQuery({
queryKey: integrationQueryKeys.getIntegrations(),
@ -128,6 +137,26 @@ export const useDeleteIntegration = () => {
});
};
export const useGetIntegration = (
integrationId: string,
options?: Omit<
UseQueryOptions<
TIntegrationWithEnv,
unknown,
TIntegrationWithEnv,
ReturnType<typeof integrationQueryKeys.getIntegration>
>,
"queryFn" | "queryKey"
>
) => {
return useQuery({
...options,
enabled: Boolean(integrationId && options?.enabled === undefined ? true : options?.enabled),
queryKey: integrationQueryKeys.getIntegration(integrationId),
queryFn: () => fetchIntegration(integrationId)
});
};
export const useSyncIntegration = () => {
return useMutation<{}, {}, { id: string; workspaceId: string; lastUsed: string }>({
mutationFn: ({ id }) => apiRequest.post(`/api/v1/integration/${id}/sync`),

View File

@ -36,14 +36,34 @@ export type TIntegration = {
metadata?: {
githubVisibility?: string;
githubVisibilityRepoIds?: string[];
shouldAutoRedeploy?: boolean;
secretAWSTag?: {
key: string;
value: string;
}[];
kmsKeyId?: string;
secretSuffix?: string;
secretPrefix?: string;
syncBehavior?: IntegrationSyncBehavior;
mappingBehavior?: IntegrationMappingBehavior;
scope: string;
org: string;
project: string;
environment: string;
shouldDisableDelete?: boolean;
shouldMaskSecrets?: boolean;
shouldProtectSecrets?: boolean;
shouldEnableDelete?: boolean;
};
};
export type TIntegrationWithEnv = TIntegration & {
environment: {
id: string;
name: string;
slug: string;
};
};

View File

@ -0,0 +1,23 @@
import { useTranslation } from "react-i18next";
import Head from "next/head";
import { IntegrationDetailsPage } from "@app/views/IntegrationsPage/IntegrationDetailsPage";
export default function IntegrationsDetailsPage() {
const { t } = useTranslation();
return (
<>
<Head>
<title>Integration Details | Infisical</title>
<link rel="icon" href="/infisical.ico" />
<meta property="og:image" content="/images/message.png" />
<meta property="og:title" content="Manage your .env files in seconds" />
<meta name="og:description" content={t("integrations.description") as string} />
</Head>
<IntegrationDetailsPage />
</>
);
}
IntegrationsDetailsPage.requireAuth = true;

View File

@ -0,0 +1,120 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { useRouter } from "next/router";
import { faChevronLeft, faEllipsis, faRefresh, faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { integrationSlugNameMapping } from "public/data/frequentConstants";
import { twMerge } from "tailwind-merge";
import { OrgPermissionCan } from "@app/components/permissions";
import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
Tooltip
} from "@app/components/v2";
import {
OrgPermissionActions,
OrgPermissionSubjects,
useOrganization,
useUser,
useWorkspace
} from "@app/context";
import { useGetIntegration } from "@app/hooks/api";
import { useSyncIntegration } from "@app/hooks/api/integrations/queries";
import { IntegrationAuditLogsSection } from "./components/IntegrationAuditLogsSection";
import { IntegrationConnectionSection } from "./components/IntegrationConnectionSection";
import { IntegrationDetailsSection } from "./components/IntegrationDetailsSection";
import { IntegrationSettingsSection } from "./components/IntegrationSettingsSection";
export const IntegrationDetailsPage = () => {
const router = useRouter();
const integrationId = router.query.integrationId as string;
const { data: integration } = useGetIntegration(integrationId, {
refetchInterval: 4000
});
const projectId = useWorkspace().currentWorkspace?.id;
const { mutateAsync: syncIntegration } = useSyncIntegration();
const { currentOrg } = useOrganization();
return integration ? (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-7xl py-6 px-6">
<Button
variant="link"
type="submit"
leftIcon={<FontAwesomeIcon icon={faChevronLeft} />}
onClick={() => {
router.push(`/integrations/${projectId}`);
}}
className="mb-4"
>
Integrations
</Button>
<div className="mb-4 flex items-center justify-between">
<p className="text-3xl font-semibold text-white">
{integrationSlugNameMapping[integration.integration]} Integration
</p>
<DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
<Tooltip content="More options">
<FontAwesomeIcon size="sm" icon={faEllipsis} />
</Tooltip>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
<DropdownMenuItem
onClick={async () => {
await syncIntegration({
id: integration.id,
lastUsed: integration.lastUsed!,
workspaceId: projectId!
});
}}
>
<div className="flex items-center gap-2">
<FontAwesomeIcon icon={faRefresh} />
Manually Sync
</div>
</DropdownMenuItem>
<OrgPermissionCan I={OrgPermissionActions.Delete} a={OrgPermissionSubjects.Member}>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
isAllowed
? "hover:!bg-red-500 hover:!text-white"
: "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={() => {}}
disabled={!isAllowed}
>
<div className="flex items-center gap-2">
<FontAwesomeIcon icon={faTrash} />
Delete Integration
</div>
</DropdownMenuItem>
)}
</OrgPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="flex justify-center">
<div className="mr-4 w-96">
<IntegrationDetailsSection integration={integration} />
<IntegrationConnectionSection integration={integration} />
</div>
<div className="space-y-4">
<IntegrationSettingsSection integration={integration} />
<IntegrationAuditLogsSection orgId={currentOrg?.id || ""} integration={integration} />
</div>
</div>
</div>
</div>
) : null;
};

View File

@ -0,0 +1,78 @@
import Link from "next/link";
import { EmptyState } from "@app/components/v2";
import { useSubscription } from "@app/context";
import { EventType } from "@app/hooks/api/auditLogs/enums";
import { TIntegrationWithEnv } from "@app/hooks/api/integrations/types";
import { LogsSection } from "@app/views/Project/AuditLogsPage/components";
// Add more events if needed
const INTEGRATION_EVENTS = [EventType.INTEGRATION_SYNCED];
type Props = {
integration: TIntegrationWithEnv;
orgId: string;
};
export const IntegrationAuditLogsSection = ({ integration, orgId }: Props) => {
const { subscription, isLoading } = useSubscription();
const auditLogsRetentionDays = subscription?.auditLogsRetentionDays ?? 30;
// eslint-disable-next-line no-nested-ternary
return subscription?.auditLogs ? (
<div className="h-full w-full min-w-[51rem] rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex items-center justify-between border-b border-mineshaft-400 pb-4">
<p className="text-lg font-semibold text-gray-200">Integration Logs</p>
<p className="text-xs text-gray-400">
Displaying audit logs from the last {auditLogsRetentionDays} days
</p>
</div>
<LogsSection
refetchInterval={4000}
remappedHeaders={{
Metadata: "Sync Status"
}}
showFilters={false}
presets={{
eventMetadata: { integrationId: integration.id },
startDate: new Date(new Date().setDate(new Date().getDate() - auditLogsRetentionDays)),
eventType: INTEGRATION_EVENTS
}}
filterClassName="bg-mineshaft-900 static"
/>
</div>
) : !isLoading ? (
<div className="h-full w-full min-w-[51rem] rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4 opacity-60">
<div className="mb-4 flex items-center justify-between border-b border-mineshaft-400 pb-4">
<p className="text-lg font-semibold text-gray-200">Integration Logs</p>
</div>
<EmptyState
className="rounded-lg"
title={
<div>
<p>
Please{" "}
<Link
href={
subscription && subscription.slug !== null
? `/org${orgId}/billing`
: "https://infisical.com/scheduledemo"
}
passHref
>
<a
className="cursor-pointer font-medium text-primary-500 transition-all hover:text-primary-600"
target="_blank"
>
upgrade your subscription
</a>
</Link>{" "}
to view integration logs
</p>
</div>
}
/>
</div>
) : null;
};

View File

@ -0,0 +1,194 @@
import { integrationSlugNameMapping } from "public/data/frequentConstants";
import { FormLabel } from "@app/components/v2";
import { IntegrationMappingBehavior, TIntegrationWithEnv } from "@app/hooks/api/integrations/types";
type Props = {
integration: TIntegrationWithEnv;
};
export const IntegrationConnectionSection = ({ integration }: Props) => {
const specifcQoveryDetails = () => {
if (integration.integration !== "qovery") return null;
return (
<div className="flex flex-row">
<div className="flex flex-col">
<FormLabel className="text-sm font-semibold text-mineshaft-300" label="Organization" />
<div className="text-sm text-mineshaft-300">{integration?.owner || "-"}</div>
</div>
<div className="flex flex-col">
<FormLabel className="text-sm font-semibold text-mineshaft-300" label="Project" />
<div className="text-sm text-mineshaft-300">{integration?.targetService || "-"}</div>
</div>
<div className="flex flex-col">
<FormLabel
className="text-sm font-semibold text-mineshaft-300"
label="Target Environment"
/>
<div className="text-sm text-mineshaft-300">{integration?.targetEnvironment || "-"}</div>
</div>
</div>
);
};
const isNotAwsManagerOneToOneDetails = () => {
const isAwsSecretManagerOneToOne =
integration.integration === "aws-secret-manager" &&
integration.metadata?.mappingBehavior === IntegrationMappingBehavior.ONE_TO_ONE;
if (isAwsSecretManagerOneToOne) {
return null;
}
const formLabel = () => {
switch (integration.integration) {
case "qovery":
return integration.scope;
case "circleci":
case "terraform-cloud":
return "Project";
case "aws-secret-manager":
return "Secret";
case "aws-parameter-store":
case "rundeck":
return "Path";
case "github":
if (["github-env", "github-repo"].includes(integration.scope!)) {
return "Repository";
}
return "Organization";
default:
return "App";
}
};
const contents = () => {
switch (integration.integration) {
case "hashicorp-vault":
return `${integration.app} - path: ${integration.path}`;
case "github":
if (integration.scope === "github-org") {
return `${integration.owner}`;
}
return `${integration.owner}/${integration.app}`;
case "aws-parameter-store":
case "rundeck":
return `${integration.path}`;
default:
return `${integration.app}`;
}
};
return (
<div className="flex flex-col">
<FormLabel className="text-sm font-semibold text-mineshaft-300" label={formLabel()} />
<div className="text-sm text-mineshaft-300">{contents()}</div>
</div>
);
};
const targetEnvironmentDetails = () => {
if (
["vercel", "netlify", "railway", "gitlab", "teamcity", "bitbucket"].includes(
integration.integration
) ||
(integration.integration === "github" && integration.scope === "github-env")
) {
return (
<div className="flex flex-col">
<FormLabel
className="text-sm font-semibold text-mineshaft-300"
label="Target Environment"
/>
<div className="text-sm text-mineshaft-300">
{integration.targetEnvironment || integration.targetEnvironmentId}
</div>
</div>
);
}
return null;
};
const generalIntegrationSpecificDetails = () => {
if (integration.integration === "checkly" && integration.targetService) {
return (
<div>
<FormLabel className="text-sm font-semibold text-mineshaft-300" label="Group" />
<div className="text-sm text-mineshaft-300">{integration.targetService}</div>
</div>
);
}
if (integration.integration === "circleci" && integration.owner) {
return (
<div>
<FormLabel className="text-sm font-semibold text-mineshaft-300" label="Organization" />
<div className="text-sm text-mineshaft-300">{integration.owner}</div>
</div>
);
}
if (integration.integration === "terraform-cloud" && integration.targetService) {
return (
<div>
<FormLabel className="text-sm font-semibold text-mineshaft-300" label="Category" />
<div className="text-sm text-mineshaft-300">{integration.targetService}</div>
</div>
);
}
if (integration.integration === "checkly" || integration.integration === "github") {
return (
<div>
<FormLabel className="text-sm font-semibold text-mineshaft-300" label="Secret Suffix" />
<div className="text-sm text-mineshaft-300">
{integration?.metadata?.secretSuffix || "-"}
</div>
</div>
);
}
return null;
};
return (
<div className="mt-4 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
<h3 className="text-lg font-semibold text-mineshaft-100">Connection</h3>
</div>
<div className="mt-4">
<FormLabel className="my-2" label="Source" />
<div className="space-y-2 rounded-lg border border-mineshaft-700 bg-mineshaft-800 p-2">
<div className="flex flex-col">
<FormLabel className="text-sm font-semibold text-mineshaft-300" label="Environment" />
<div className="text-sm text-mineshaft-300">{integration.environment.name}</div>
</div>
<div className="flex flex-col">
<FormLabel className="text-sm font-semibold text-mineshaft-300" label="Secret Path" />
<div className="text-sm text-mineshaft-300">{integration.secretPath}</div>
</div>
</div>
<FormLabel className="my-2" label="Destination" />
<div className="space-y-2 rounded-lg border border-mineshaft-700 bg-mineshaft-800 p-2">
<FormLabel className="text-sm font-semibold text-mineshaft-300" label="Platform" />
<div className="text-sm text-mineshaft-300">
{integrationSlugNameMapping[integration.integration]}
</div>
{specifcQoveryDetails()}
{isNotAwsManagerOneToOneDetails()}
{targetEnvironmentDetails()}
{generalIntegrationSpecificDetails()}
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,69 @@
import { faCalendarCheck, faCheckCircle, faCircleXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { format } from "date-fns";
import { integrationSlugNameMapping } from "public/data/frequentConstants";
import { twMerge } from "tailwind-merge";
import { TIntegrationWithEnv } from "@app/hooks/api/integrations/types";
type Props = {
integration: TIntegrationWithEnv;
};
export const IntegrationDetailsSection = ({ integration }: Props) => {
return (
<div>
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
<h3 className="text-lg font-semibold text-mineshaft-100">Integration Details</h3>
</div>
<div className="mt-4">
<div className="space-y-3">
<div>
<p className="text-sm font-semibold text-mineshaft-300">Name</p>
<p className="text-sm text-mineshaft-300">
{integrationSlugNameMapping[integration.integration]}
</p>
</div>
<div>
<p className="text-sm font-semibold text-mineshaft-300">Sync Status</p>
<div className="flex items-center">
<p
className={twMerge(
"mr-2 text-sm font-medium",
integration.isSynced ? "text-green-500" : "text-red-500"
)}
>
{integration.isSynced ? "Synced" : "Not Synced"}
</p>
<FontAwesomeIcon
size="sm"
className={twMerge(integration.isSynced ? "text-green-500" : "text-red-500")}
icon={integration.isSynced ? faCheckCircle : faCircleXmark}
/>
</div>
</div>
{integration.lastUsed && (
<div>
<p className="text-sm font-semibold text-mineshaft-300">Latest Successful Sync</p>
<div className="flex items-center gap-2 text-sm text-mineshaft-300">
{format(new Date(integration.lastUsed), "yyyy-MM-dd, hh:mm aaa")}
<FontAwesomeIcon icon={faCalendarCheck} className="pt-0.5 pr-2 text-sm" />
</div>
</div>
)}
<div>
{!integration.isSynced && integration.syncMessage && (
<>
<p className="text-sm font-semibold text-mineshaft-300">Latest Sync Error</p>
<p className="text-sm text-mineshaft-300">{integration.syncMessage}</p>
</>
)}
</div>
</div>
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,89 @@
import { TIntegrationWithEnv } from "@app/hooks/api/integrations/types";
type Props = {
integration: TIntegrationWithEnv;
};
type Metadata = NonNullable<TIntegrationWithEnv["metadata"]>;
type MetadataKey = keyof Metadata;
type MetadataValue<K extends MetadataKey> = Metadata[K];
const metadataMappings: Record<keyof NonNullable<TIntegrationWithEnv["metadata"]>, string> = {
githubVisibility: "Github Visibility",
githubVisibilityRepoIds: "Github Visibility Repo Ids",
shouldAutoRedeploy: "Auto Redeploy Target Application When Secrets Change",
secretAWSTag: "Tags For Secrets Stored In AWS",
kmsKeyId: "AWS KMS Key ID",
secretSuffix: "Secret Suffix",
secretPrefix: "Secret Prefix",
syncBehavior: "Secrets Sync behavior",
mappingBehavior: "Secrets Mapping Behavior",
scope: "Scope",
org: "Organization",
project: "Project",
environment: "Environment",
shouldDisableDelete: "AWS Secret Deletion Disabled",
shouldMaskSecrets: "GitLab Secrets Masking Enabled",
shouldProtectSecrets: "GitLab Secret Protection Enabled",
shouldEnableDelete: "GitHub Secret Deletion Enabled"
} as const;
export const IntegrationSettingsSection = ({ integration }: Props) => {
const renderValue = <K extends MetadataKey>(key: K, value: MetadataValue<K>) => {
if (!value) return null;
// If it's a boolean, we render a generic "Yes" or "No" response.
if (typeof value === "boolean") {
return value ? "Yes" : "No";
}
// When the value is an object or array, or array of objects, we need to handle some special cases.
if (typeof value === "object") {
if (key === "secretAWSTag") {
return (value as MetadataValue<"secretAWSTag">)!.map(({ key: tagKey, value: tagValue }) => (
<p key={tagKey} className="text-sm text-gray-200">
{tagKey}={tagValue}
</p>
));
}
if (key === "githubVisibilityRepoIds") {
return value.join(", ");
}
}
if (typeof value === "string") {
return value.length ? value : "N/A";
}
if (typeof value === "number") {
return value;
}
return null;
};
if (!integration.metadata || Object.keys(integration.metadata).length === 0) {
return null;
}
// eslint-disable-next-line no-nested-ternary
return (
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex items-center justify-between border-b border-mineshaft-400 pb-4">
<p className="text-lg font-semibold text-gray-200">Integration Settings</p>
</div>
<div className="grid grid-cols-2 gap-4">
{integration.metadata &&
Object.entries(integration.metadata).map(([key, value]) => (
<div key={key} className="flex flex-col">
<p className="text-sm text-gray-400">
{metadataMappings[key as keyof typeof metadataMappings]}
</p>
<p className="text-sm text-gray-200">{renderValue(key as MetadataKey, value)}</p>
</div>
))}
</div>
</div>
);
};

View File

@ -0,0 +1 @@
export { IntegrationDetailsPage } from "./IntegrationDetailsPage";

View File

@ -1,6 +1,10 @@
/* eslint-disable jsx-a11y/click-events-have-key-events */
/* eslint-disable jsx-a11y/no-static-element-interactions */
import { useRouter } from "next/router";
import {
faArrowRight,
faCalendarCheck,
faEllipsis,
faRefresh,
faWarning,
faXmark
@ -10,7 +14,7 @@ import { format } from "date-fns";
import { integrationSlugNameMapping } from "public/data/frequentConstants";
import { ProjectPermissionCan } from "@app/components/permissions";
import { Button, FormLabel, IconButton, Tag, Tooltip } from "@app/components/v2";
import { Badge, FormLabel, IconButton, Tooltip } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { IntegrationMappingBehavior } from "@app/hooks/api/integrations/types";
import { TIntegration } from "@app/hooks/api/types";
@ -28,9 +32,12 @@ export const ConfiguredIntegrationItem = ({
onRemoveIntegration,
onManualSyncIntegration
}: IProps) => {
const router = useRouter();
return (
<div
className="max-w-8xl flex justify-between rounded-md border border-mineshaft-600 bg-mineshaft-800 p-3"
className="max-w-8xl flex cursor-pointer justify-between rounded-md border border-mineshaft-600 bg-mineshaft-800 p-3 transition-all hover:bg-mineshaft-700"
onClick={() => router.push(`/integrations/details/${integration.id}`)}
key={`integration-${integration?.id.toString()}`}
>
<div className="flex">
@ -168,9 +175,9 @@ export const ConfiguredIntegrationItem = ({
</div>
)}
</div>
<div className="mt-[1.5rem] flex cursor-default">
<div className="mt-[1.5rem] flex cursor-default space-x-3">
{integration.isSynced != null && integration.lastUsed != null && (
<Tag key={integration.id} className={integration.isSynced ? "bg-green-800" : "bg-red/80"}>
<Badge variant={integration.isSynced ? "success" : "danger"} key={integration.id}>
<Tooltip
center
className="max-w-xs whitespace-normal break-words"
@ -178,7 +185,7 @@ export const ConfiguredIntegrationItem = ({
<div className="flex max-h-[10rem] flex-col overflow-auto ">
<div className="flex self-start">
<FontAwesomeIcon icon={faCalendarCheck} className="pt-0.5 pr-2 text-sm" />
<div className="text-sm">Last sync</div>
<div className="text-sm">Last successful sync</div>
</div>
<div className="pl-5 text-left text-xs">
{format(new Date(integration.lastUsed), "yyyy-MM-dd, hh:mm aaa")}
@ -195,43 +202,62 @@ export const ConfiguredIntegrationItem = ({
</div>
}
>
<div className="flex items-center space-x-2 text-white">
<div className="flex h-full items-center space-x-2">
<div>{integration.isSynced ? "Synced" : "Not synced"}</div>
{!integration.isSynced && <FontAwesomeIcon icon={faWarning} />}
</div>
</Tooltip>
</Tag>
</Badge>
)}
<div className="mr-1 flex items-end opacity-80 duration-200 hover:opacity-100">
<div className="space-x-1.5">
<Tooltip className="text-center" content="Manually sync integration secrets">
<Button
onClick={() => onManualSyncIntegration()}
<IconButton
onClick={(e) => {
e.stopPropagation();
onManualSyncIntegration();
}}
ariaLabel="sync"
colorSchema="primary"
variant="star"
className="max-w-[2.5rem] border-none bg-mineshaft-500"
>
<FontAwesomeIcon icon={faRefresh} className="px-1 text-bunker-200" />
</Button>
<FontAwesomeIcon icon={faRefresh} className="px-1" />
</IconButton>
</Tooltip>
</div>
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={ProjectPermissionSub.Integrations}
>
{(isAllowed: boolean) => (
<div className="flex items-end opacity-80 duration-200 hover:opacity-100">
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={ProjectPermissionSub.Integrations}
>
{(isAllowed: boolean) => (
<Tooltip content="Remove Integration">
<IconButton
onClick={() => onRemoveIntegration()}
onClick={(e) => {
e.stopPropagation();
onRemoveIntegration();
}}
ariaLabel="delete"
isDisabled={!isAllowed}
colorSchema="danger"
variant="star"
className="max-w-[2.5rem] border-none bg-mineshaft-500"
>
<FontAwesomeIcon icon={faXmark} className="px-0.5" />
<FontAwesomeIcon icon={faXmark} className="px-1" />
</IconButton>
</Tooltip>
</div>
)}
</ProjectPermissionCan>
)}
</ProjectPermissionCan>
<Tooltip content="View details">
<IconButton
ariaLabel="delete"
colorSchema="primary"
variant="star"
className="max-w-[2.5rem] border-none bg-mineshaft-500"
>
<FontAwesomeIcon icon={faEllipsis} className="px-1" />
</IconButton>
</Tooltip>
</div>
</div>
</div>
);

View File

@ -42,7 +42,9 @@ export const UserAuditLogsSection = withPermission(
<LogsSection
showFilters={showFilter}
filterClassName="bg-mineshaft-900 static"
presetActor={orgMembership.user.id}
presets={{
actorId: orgMembership.user.id
}}
isOrgAuditLogs
/>
</div>

View File

@ -1,14 +1,29 @@
/* eslint-disable no-nested-ternary */
import { useState } from "react";
import { Control, Controller, UseFormReset } from "react-hook-form";
import { faFilterCircleXmark } from "@fortawesome/free-solid-svg-icons";
import { Control, Controller, UseFormReset, UseFormWatch } from "react-hook-form";
import {
faCheckCircle,
faChevronDown,
faFilterCircleXmark
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
import { Button, DatePicker, FormControl, Select, SelectItem } from "@app/components/v2";
import {
Button,
DatePicker,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
FormControl,
Select,
SelectItem
} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { useGetAuditLogActorFilterOpts } from "@app/hooks/api";
import { eventToNameMap, userAgentTTypeoNameMap } from "@app/hooks/api/auditLogs/constants";
import { ActorType } from "@app/hooks/api/auditLogs/enums";
import { ActorType, EventType } from "@app/hooks/api/auditLogs/enums";
import { Actor } from "@app/hooks/api/auditLogs/types";
import { AuditLogFilterFormData } from "./types";
@ -20,13 +35,17 @@ const userAgentTypes = Object.entries(userAgentTTypeoNameMap).map(([value, label
}));
type Props = {
presetActor?: string;
presets?: {
actorId?: string;
eventType?: EventType[];
};
className?: string;
control: Control<AuditLogFilterFormData>;
reset: UseFormReset<AuditLogFilterFormData>;
watch: UseFormWatch<AuditLogFilterFormData>;
};
export const LogsFilter = ({ presetActor, className, control, reset }: Props) => {
export const LogsFilter = ({ presets, className, control, reset, watch }: Props) => {
const [isStartDatePickerOpen, setIsStartDatePickerOpen] = useState(false);
const [isEndDatePickerOpen, setIsEndDatePickerOpen] = useState(false);
@ -71,6 +90,8 @@ export const LogsFilter = ({ presetActor, className, control, reset }: Props) =>
}
};
const selectedEventTypes = watch("eventType") as EventType[] | undefined;
return (
<div
className={twMerge(
@ -82,29 +103,69 @@ export const LogsFilter = ({ presetActor, className, control, reset }: Props) =>
<Controller
control={control}
name="eventType"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Event"
errorText={error?.message}
isError={Boolean(error)}
className="w-40"
>
<Select
{...(field.value ? { value: field.value } : { placeholder: "Select" })}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full border border-mineshaft-500 bg-mineshaft-700 text-mineshaft-100"
>
{eventTypes.map(({ label, value }) => (
<SelectItem value={String(value || "")} key={label}>
{label}
</SelectItem>
))}
</Select>
render={({ field }) => (
<FormControl label="Events">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<div className="inline-flex w-full cursor-pointer items-center justify-between rounded-md border border-mineshaft-500 bg-mineshaft-700 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200">
{selectedEventTypes?.length === 1
? eventTypes.find((eventType) => eventType.value === selectedEventTypes[0])
?.label
: selectedEventTypes?.length === 0
? "Select event types"
: `${selectedEventTypes?.length} events selected`}
<FontAwesomeIcon icon={faChevronDown} className="ml-2 text-xs" />
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="z-[100] max-h-80 overflow-hidden">
<div className="max-h-80 overflow-y-auto">
{eventTypes && eventTypes.length > 0 ? (
eventTypes.map((eventType) => {
const isSelected = selectedEventTypes?.includes(
eventType.value as EventType
);
return (
<DropdownMenuItem
onSelect={(event) => eventTypes.length > 1 && event.preventDefault()}
onClick={() => {
if (selectedEventTypes?.includes(eventType.value as EventType)) {
field.onChange(
selectedEventTypes?.filter((e: string) => e !== eventType.value)
);
} else {
field.onChange([...(selectedEventTypes || []), eventType.value]);
}
}}
key={`event-type-${eventType.value}`}
icon={
isSelected ? (
<FontAwesomeIcon
icon={faCheckCircle}
className="pr-0.5 text-primary"
/>
) : (
<div className="pl-[1.01rem]" />
)
}
iconPos="left"
className="w-[28.4rem] text-sm"
>
{eventType.label}
</DropdownMenuItem>
);
})
) : (
<div />
)}
</div>
</DropdownMenuContent>
</DropdownMenu>
</FormControl>
)}
/>
{!isLoading && data && data.length > 0 && !presetActor && (
{!isLoading && data && data.length > 0 && !presets?.actorId && (
<Controller
control={control}
name="actor"
@ -207,8 +268,8 @@ export const LogsFilter = ({ presetActor, className, control, reset }: Props) =>
leftIcon={<FontAwesomeIcon icon={faFilterCircleXmark} />}
onClick={() =>
reset({
eventType: undefined,
actor: presetActor,
eventType: presets?.eventType || [],
actor: presets?.actorId,
userAgentType: undefined,
startDate: undefined,
endDate: undefined

View File

@ -5,24 +5,38 @@ import { yupResolver } from "@hookform/resolvers/yup";
import { UpgradePlanModal } from "@app/components/v2";
import { useSubscription } from "@app/context";
import { EventType, UserAgentType } from "@app/hooks/api/auditLogs/enums";
import { ActorType, EventType, UserAgentType } from "@app/hooks/api/auditLogs/enums";
import { usePopUp } from "@app/hooks/usePopUp";
import { LogsFilter } from "./LogsFilter";
import { LogsTable } from "./LogsTable";
import { LogsTable, TAuditLogTableHeader } from "./LogsTable";
import { AuditLogFilterFormData, auditLogFilterFormSchema } from "./types";
type Props = {
presetActor?: string;
presets?: {
actorId?: string;
eventType?: EventType[];
actorType?: ActorType;
startDate?: Date;
endDate?: Date;
eventMetadata?: Record<string, string>;
};
showFilters?: boolean;
filterClassName?: string;
isOrgAuditLogs?: boolean;
showActorColumn?: boolean;
remappedHeaders?: Partial<Record<TAuditLogTableHeader, string>>;
refetchInterval?: number;
};
export const LogsSection = ({
presetActor,
presets,
filterClassName,
remappedHeaders,
isOrgAuditLogs,
showActorColumn,
refetchInterval,
showFilters
}: Props) => {
const { subscription } = useSubscription();
@ -33,11 +47,12 @@ export const LogsSection = ({
const { control, reset, watch } = useForm<AuditLogFilterFormData>({
resolver: yupResolver(auditLogFilterFormSchema),
defaultValues: {
actor: presetActor,
actor: presets?.actorId,
eventType: presets?.eventType || [],
page: 1,
perPage: 10,
startDate: new Date(new Date().setDate(new Date().getDate() - 1)), // day before today
endDate: new Date(new Date(Date.now()).setHours(23, 59, 59, 999)) // end of today
startDate: presets?.startDate ?? new Date(new Date().setDate(new Date().getDate() - 1)), // day before today
endDate: presets?.endDate ?? new Date(new Date(Date.now()).setHours(23, 59, 59, 999)) // end of today
}
});
@ -47,7 +62,7 @@ export const LogsSection = ({
}
}, [subscription]);
const eventType = watch("eventType") as EventType | undefined;
const eventType = watch("eventType") as EventType[] | undefined;
const userAgentType = watch("userAgentType") as UserAgentType | undefined;
const actor = watch("actor");
@ -59,19 +74,27 @@ export const LogsSection = ({
{showFilters && (
<LogsFilter
className={filterClassName}
presetActor={presetActor}
presets={presets}
control={control}
watch={watch}
reset={reset}
/>
)}
<LogsTable
refetchInterval={refetchInterval}
remappedHeaders={remappedHeaders}
isOrgAuditLogs={isOrgAuditLogs}
eventType={eventType}
userAgentType={userAgentType}
showActorColumn={!presetActor}
actor={actor}
startDate={startDate}
endDate={endDate}
showActorColumn={!!showActorColumn && !isOrgAuditLogs}
filter={{
eventMetadata: presets?.eventMetadata,
actorType: presets?.actorType,
limit: 15,
eventType,
userAgentType,
startDate,
endDate,
actorId: actor
}}
/>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}

View File

@ -15,43 +15,41 @@ import {
} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { useGetAuditLogs } from "@app/hooks/api";
import { EventType, UserAgentType } from "@app/hooks/api/auditLogs/enums";
import { TGetAuditLogsFilter } from "@app/hooks/api/auditLogs/types";
import { LogsTableRow } from "./LogsTableRow";
type Props = {
eventType?: EventType;
userAgentType?: UserAgentType;
actor?: string;
startDate?: Date;
endDate?: Date;
isOrgAuditLogs?: boolean;
showActorColumn: boolean;
filter?: TGetAuditLogsFilter;
remappedHeaders?: Partial<Record<TAuditLogTableHeader, string>>;
refetchInterval?: number;
};
const AUDIT_LOG_LIMIT = 15;
const TABLE_HEADERS = ["Timestamp", "Event", "Project", "Actor", "Source", "Metadata"] as const;
export type TAuditLogTableHeader = (typeof TABLE_HEADERS)[number];
export const LogsTable = ({
eventType,
userAgentType,
showActorColumn,
actor,
startDate,
endDate,
isOrgAuditLogs
isOrgAuditLogs,
filter,
remappedHeaders,
refetchInterval
}: Props) => {
const { currentWorkspace } = useWorkspace();
const { data, isLoading, isFetchingNextPage, hasNextPage, fetchNextPage } = useGetAuditLogs(
{
eventType,
userAgentType,
actor,
startDate,
endDate,
...filter,
limit: AUDIT_LOG_LIMIT
},
!isOrgAuditLogs ? currentWorkspace?.id ?? "" : null
!isOrgAuditLogs ? currentWorkspace?.id ?? "" : null,
{
refetchInterval
}
);
const isEmpty = !isLoading && !data?.pages?.[0].length;
@ -62,18 +60,24 @@ export const LogsTable = ({
<Table>
<THead>
<Tr>
<Th>Timestamp</Th>
<Th>Event</Th>
{isOrgAuditLogs && <Th>Project</Th>}
{showActorColumn && <Th>Actor</Th>}
<Th>Source</Th>
<Th>Metadata</Th>
{TABLE_HEADERS.map((header, idx) => {
if (
(header === "Project" && !isOrgAuditLogs) ||
(header === "Actor" && !showActorColumn)
) {
return null;
}
return (
<Th key={`table-header-${idx + 1}`}>{remappedHeaders?.[header] || header}</Th>
);
})}
</Tr>
</THead>
<TBody>
{!isLoading &&
data?.pages?.map((group, i) => (
<Fragment key={`auditlog-item-${i + 1}`}>
<Fragment key={`audit-log-fragment-${i + 1}`}>
{group.map((auditLog) => (
<LogsTableRow
showActorColumn={showActorColumn}

View File

@ -1,4 +1,4 @@
import { Td, Tr } from "@app/components/v2";
import { Badge, Td, Tooltip, Tr } from "@app/components/v2";
import { eventToNameMap, userAgentTTypeoNameMap } from "@app/hooks/api/auditLogs/constants";
import { ActorType, EventType } from "@app/hooks/api/auditLogs/enums";
import { Actor, AuditLog, Event } from "@app/hooks/api/auditLogs/types";
@ -461,6 +461,21 @@ export const LogsTableRow = ({ auditLog, isOrgAuditLogs, showActorColumn }: Prop
<p>{`Secret Request Channels: ${event.metadata.secretRequestChannels}`}</p>
</Td>
);
case EventType.INTEGRATION_SYNCED:
return (
<Td>
<Tooltip
className="max-w-xs whitespace-normal break-words"
content={event.metadata.syncMessage!}
isDisabled={!event.metadata.syncMessage}
>
<Badge variant={event.metadata.isSynced ? "success" : "danger"}>
<p className="text-center">{event.metadata.isSynced ? "Successful" : "Failed"}</p>
</Badge>
</Tooltip>
</Td>
);
default:
return <Td />;
}
@ -484,16 +499,41 @@ export const LogsTableRow = ({ auditLog, isOrgAuditLogs, showActorColumn }: Prop
return formattedDate;
};
const renderSource = () => {
const { event, actor } = auditLog;
if (event.type === EventType.INTEGRATION_SYNCED) {
if (actor.type === ActorType.USER) {
return (
<Td>
<p>Manually triggered by {actor.metadata.email}</p>
</Td>
);
}
// Platform / automatic syncs
return (
<Td>
<p>Automatically synced by Infisical</p>
</Td>
);
}
return (
<Td>
<p>{userAgentTTypeoNameMap[auditLog.userAgentType]}</p>
<p>{auditLog.ipAddress}</p>
</Td>
);
};
return (
<Tr className={`log-${auditLog.id} h-10 border-x-0 border-b border-t-0`}>
<Td>{formatDate(auditLog.createdAt)}</Td>
<Td>{`${eventToNameMap[auditLog.event.type]}`}</Td>
{isOrgAuditLogs && <Td>{auditLog.project.name}</Td>}
{showActorColumn && renderActor(auditLog.actor)}
<Td>
<p>{userAgentTTypeoNameMap[auditLog.userAgentType]}</p>
<p>{auditLog.ipAddress}</p>
</Td>
{renderSource()}
{renderMetadata(auditLog.event)}
</Tr>
);

View File

@ -4,7 +4,8 @@ import { EventType, UserAgentType } from "@app/hooks/api/auditLogs/enums";
export const auditLogFilterFormSchema = yup
.object({
eventType: yup.string().oneOf(Object.values(EventType), "Invalid event type"),
eventMetadata: yup.object({}).optional(),
eventType: yup.array(yup.string().oneOf(Object.values(EventType), "Invalid event type")),
actor: yup.string(),
userAgentType: yup.string().oneOf(Object.values(UserAgentType), "Invalid user agent type"),
startDate: yup.date(),