mirror of
https://github.com/Infisical/infisical.git
synced 2025-07-05 04:29:09 +00:00
Compare commits
22 Commits
daniel/bet
...
v0.85.0-po
Author | SHA1 | Date | |
---|---|---|---|
d6b7045461 | |||
bd9c9ea1f4 | |||
5740d2b4e4 | |||
09887a7405 | |||
38ee3a005e | |||
74653e7ed1 | |||
8a0b1bb427 | |||
1f6faadf81 | |||
8f3b7e1698 | |||
24c460c695 | |||
8acceab1e7 | |||
d60aba9339 | |||
3a228f7521 | |||
3f7ac0f142 | |||
63cf535ebb | |||
69a2a46c47 | |||
d081077273 | |||
75034f9350 | |||
eacd7b0c6a | |||
5bad77083c | |||
1025759efb | |||
5e5ab29ab9 |
@ -146,12 +146,16 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
projectId: req.params.workspaceId,
|
actor: req.permission.type,
|
||||||
...req.query,
|
|
||||||
endDate: req.query.endDate,
|
filter: {
|
||||||
startDate: req.query.startDate || getLastMidnightDateISO(),
|
...req.query,
|
||||||
auditLogActor: req.query.actor,
|
projectId: req.params.workspaceId,
|
||||||
actor: req.permission.type
|
endDate: req.query.endDate,
|
||||||
|
startDate: req.query.startDate || getLastMidnightDateISO(),
|
||||||
|
auditLogActorId: req.query.actor,
|
||||||
|
eventType: req.query.eventType ? [req.query.eventType] : undefined
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return { auditLogs };
|
return { auditLogs };
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,9 @@ import { DatabaseError } from "@app/lib/errors";
|
|||||||
import { ormify, selectAllTableCols, stripUndefinedInWhere } from "@app/lib/knex";
|
import { ormify, selectAllTableCols, stripUndefinedInWhere } from "@app/lib/knex";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
import { QueueName } from "@app/queue";
|
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>;
|
export type TAuditLogDALFactory = ReturnType<typeof auditLogDALFactory>;
|
||||||
|
|
||||||
@ -25,7 +28,24 @@ export const auditLogDALFactory = (db: TDbClient) => {
|
|||||||
const auditLogOrm = ormify(db, TableName.AuditLog);
|
const auditLogOrm = ormify(db, TableName.AuditLog);
|
||||||
|
|
||||||
const find = async (
|
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
|
tx?: Knex
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
@ -34,7 +54,6 @@ export const auditLogDALFactory = (db: TDbClient) => {
|
|||||||
stripUndefinedInWhere({
|
stripUndefinedInWhere({
|
||||||
projectId,
|
projectId,
|
||||||
[`${TableName.AuditLog}.orgId`]: orgId,
|
[`${TableName.AuditLog}.orgId`]: orgId,
|
||||||
eventType,
|
|
||||||
userAgentType
|
userAgentType
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
@ -52,8 +71,22 @@ export const auditLogDALFactory = (db: TDbClient) => {
|
|||||||
.offset(offset)
|
.offset(offset)
|
||||||
.orderBy(`${TableName.AuditLog}.createdAt`, "desc");
|
.orderBy(`${TableName.AuditLog}.createdAt`, "desc");
|
||||||
|
|
||||||
if (actor) {
|
if (actorId) {
|
||||||
void sqlQuery.whereRaw(`"actorMetadata"->>'userId' = ?`, [actor]);
|
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) {
|
if (startDate) {
|
||||||
|
@ -23,25 +23,12 @@ export const auditLogServiceFactory = ({
|
|||||||
auditLogQueue,
|
auditLogQueue,
|
||||||
permissionService
|
permissionService
|
||||||
}: TAuditLogServiceFactoryDep) => {
|
}: TAuditLogServiceFactoryDep) => {
|
||||||
const listAuditLogs = async ({
|
const listAuditLogs = async ({ actorAuthMethod, actorId, actorOrgId, actor, filter }: TListProjectAuditLogDTO) => {
|
||||||
userAgentType,
|
if (filter.projectId) {
|
||||||
eventType,
|
|
||||||
offset,
|
|
||||||
limit,
|
|
||||||
endDate,
|
|
||||||
startDate,
|
|
||||||
actor,
|
|
||||||
actorId,
|
|
||||||
actorOrgId,
|
|
||||||
actorAuthMethod,
|
|
||||||
projectId,
|
|
||||||
auditLogActor
|
|
||||||
}: TListProjectAuditLogDTO) => {
|
|
||||||
if (projectId) {
|
|
||||||
const { permission } = await permissionService.getProjectPermission(
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
projectId,
|
filter.projectId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId
|
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.
|
// If project ID is not provided, then we need to return all the audit logs for the organization itself.
|
||||||
|
|
||||||
const auditLogs = await auditLogDAL.find({
|
const auditLogs = await auditLogDAL.find({
|
||||||
startDate,
|
startDate: filter.startDate,
|
||||||
endDate,
|
endDate: filter.endDate,
|
||||||
limit,
|
limit: filter.limit,
|
||||||
offset,
|
offset: filter.offset,
|
||||||
eventType,
|
eventType: filter.eventType,
|
||||||
userAgentType,
|
userAgentType: filter.userAgentType,
|
||||||
actor: auditLogActor,
|
actorId: filter.auditLogActorId,
|
||||||
...(projectId ? { projectId } : { orgId: actorOrgId })
|
actorType: filter.actorType,
|
||||||
|
eventMetadata: filter.eventMetadata,
|
||||||
|
...(filter.projectId ? { projectId: filter.projectId } : { orgId: actorOrgId })
|
||||||
});
|
});
|
||||||
|
|
||||||
return auditLogs.map(({ eventType: logEventType, actor: eActor, actorMetadata, eventMetadata, ...el }) => ({
|
return auditLogs.map(({ eventType: logEventType, actor: eActor, actorMetadata, eventMetadata, ...el }) => ({
|
||||||
|
@ -5,19 +5,23 @@ import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
|
|||||||
import { PkiItemType } from "@app/services/pki-collection/pki-collection-types";
|
import { PkiItemType } from "@app/services/pki-collection/pki-collection-types";
|
||||||
|
|
||||||
export type TListProjectAuditLogDTO = {
|
export type TListProjectAuditLogDTO = {
|
||||||
auditLogActor?: string;
|
filter: {
|
||||||
projectId?: string;
|
userAgentType?: UserAgentType;
|
||||||
eventType?: string;
|
eventType?: EventType[];
|
||||||
startDate?: string;
|
offset?: number;
|
||||||
endDate?: string;
|
limit: number;
|
||||||
userAgentType?: string;
|
endDate?: string;
|
||||||
limit?: number;
|
startDate?: string;
|
||||||
offset?: number;
|
projectId?: string;
|
||||||
|
auditLogActorId?: string;
|
||||||
|
actorType?: ActorType;
|
||||||
|
eventMetadata?: Record<string, string>;
|
||||||
|
};
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
export type TCreateAuditLogDTO = {
|
export type TCreateAuditLogDTO = {
|
||||||
event: Event;
|
event: Event;
|
||||||
actor: UserActor | IdentityActor | ServiceActor | ScimClientActor;
|
actor: UserActor | IdentityActor | ServiceActor | ScimClientActor | PlatformActor;
|
||||||
orgId?: string;
|
orgId?: string;
|
||||||
projectId?: string;
|
projectId?: string;
|
||||||
} & BaseAuthData;
|
} & BaseAuthData;
|
||||||
@ -177,7 +181,8 @@ export enum EventType {
|
|||||||
UPDATE_SLACK_INTEGRATION = "update-slack-integration",
|
UPDATE_SLACK_INTEGRATION = "update-slack-integration",
|
||||||
DELETE_SLACK_INTEGRATION = "delete-slack-integration",
|
DELETE_SLACK_INTEGRATION = "delete-slack-integration",
|
||||||
GET_PROJECT_SLACK_CONFIG = "get-project-slack-config",
|
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 {
|
interface UserActorMetadata {
|
||||||
@ -198,6 +203,8 @@ interface IdentityActorMetadata {
|
|||||||
|
|
||||||
interface ScimClientActorMetadata {}
|
interface ScimClientActorMetadata {}
|
||||||
|
|
||||||
|
interface PlatformActorMetadata {}
|
||||||
|
|
||||||
export interface UserActor {
|
export interface UserActor {
|
||||||
type: ActorType.USER;
|
type: ActorType.USER;
|
||||||
metadata: UserActorMetadata;
|
metadata: UserActorMetadata;
|
||||||
@ -208,6 +215,11 @@ export interface ServiceActor {
|
|||||||
metadata: ServiceActorMetadata;
|
metadata: ServiceActorMetadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PlatformActor {
|
||||||
|
type: ActorType.PLATFORM;
|
||||||
|
metadata: PlatformActorMetadata;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IdentityActor {
|
export interface IdentityActor {
|
||||||
type: ActorType.IDENTITY;
|
type: ActorType.IDENTITY;
|
||||||
metadata: IdentityActorMetadata;
|
metadata: IdentityActorMetadata;
|
||||||
@ -218,7 +230,7 @@ export interface ScimClientActor {
|
|||||||
metadata: ScimClientActorMetadata;
|
metadata: ScimClientActorMetadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Actor = UserActor | ServiceActor | IdentityActor | ScimClientActor;
|
export type Actor = UserActor | ServiceActor | IdentityActor | ScimClientActor | PlatformActor;
|
||||||
|
|
||||||
interface GetSecretsEvent {
|
interface GetSecretsEvent {
|
||||||
type: EventType.GET_SECRETS;
|
type: EventType.GET_SECRETS;
|
||||||
@ -1518,6 +1530,16 @@ interface GetProjectSlackConfig {
|
|||||||
id: string;
|
id: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
interface IntegrationSyncedEvent {
|
||||||
|
type: EventType.INTEGRATION_SYNCED;
|
||||||
|
metadata: {
|
||||||
|
integrationId: string;
|
||||||
|
lastSyncJobId: string;
|
||||||
|
lastUsed: Date;
|
||||||
|
syncMessage: string;
|
||||||
|
isSynced: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export type Event =
|
export type Event =
|
||||||
| GetSecretsEvent
|
| GetSecretsEvent
|
||||||
@ -1657,4 +1679,5 @@ export type Event =
|
|||||||
| DeleteSlackIntegration
|
| DeleteSlackIntegration
|
||||||
| GetSlackIntegration
|
| GetSlackIntegration
|
||||||
| UpdateProjectSlackConfig
|
| UpdateProjectSlackConfig
|
||||||
| GetProjectSlackConfig;
|
| GetProjectSlackConfig
|
||||||
|
| IntegrationSyncedEvent;
|
||||||
|
@ -91,6 +91,8 @@ export type TQueueJobTypes = {
|
|||||||
[QueueName.IntegrationSync]: {
|
[QueueName.IntegrationSync]: {
|
||||||
name: QueueJobs.IntegrationSync;
|
name: QueueJobs.IntegrationSync;
|
||||||
payload: {
|
payload: {
|
||||||
|
isManual?: boolean;
|
||||||
|
actorId?: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
environment: string;
|
environment: string;
|
||||||
secretPath: string;
|
secretPath: string;
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
import fastifyPlugin from "fastify-plugin";
|
import fastifyPlugin from "fastify-plugin";
|
||||||
import { JsonWebTokenError } from "jsonwebtoken";
|
|
||||||
import { ZodError } from "zod";
|
import { ZodError } from "zod";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -12,12 +11,6 @@ import {
|
|||||||
UnauthorizedError
|
UnauthorizedError
|
||||||
} from "@app/lib/errors";
|
} from "@app/lib/errors";
|
||||||
|
|
||||||
enum JWTErrors {
|
|
||||||
JwtExpired = "jwt expired",
|
|
||||||
JwtMalformed = "jwt malformed",
|
|
||||||
InvalidAlgorithm = "invalid algorithm"
|
|
||||||
}
|
|
||||||
|
|
||||||
export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider) => {
|
export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider) => {
|
||||||
server.setErrorHandler((error, req, res) => {
|
server.setErrorHandler((error, req, res) => {
|
||||||
req.log.error(error);
|
req.log.error(error);
|
||||||
@ -43,27 +36,6 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider
|
|||||||
status: error.status,
|
status: error.status,
|
||||||
detail: error.detail
|
detail: error.detail
|
||||||
});
|
});
|
||||||
// Handle JWT errors and make them more human-readable for the end-user.
|
|
||||||
} else if (error instanceof JsonWebTokenError) {
|
|
||||||
const message = (() => {
|
|
||||||
if (error.message === JWTErrors.JwtExpired) {
|
|
||||||
return "Your token has expired. Please re-authenticate.";
|
|
||||||
}
|
|
||||||
if (error.message === JWTErrors.JwtMalformed) {
|
|
||||||
return "The provided access token is malformed. Please use a valid token or generate a new one and try again.";
|
|
||||||
}
|
|
||||||
if (error.message === JWTErrors.InvalidAlgorithm) {
|
|
||||||
return "The access token is signed with an invalid algorithm. Please provide a valid token and try again.";
|
|
||||||
}
|
|
||||||
|
|
||||||
return error.message;
|
|
||||||
})();
|
|
||||||
|
|
||||||
void res.status(401).send({
|
|
||||||
statusCode: 401,
|
|
||||||
error: "TokenError",
|
|
||||||
message
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
void res.send(error);
|
void res.send(error);
|
||||||
}
|
}
|
||||||
|
@ -810,6 +810,8 @@ export const registerRoutes = async (
|
|||||||
projectEnvDAL,
|
projectEnvDAL,
|
||||||
webhookDAL,
|
webhookDAL,
|
||||||
orgDAL,
|
orgDAL,
|
||||||
|
auditLogService,
|
||||||
|
userDAL,
|
||||||
projectMembershipDAL,
|
projectMembershipDAL,
|
||||||
smtpService,
|
smtpService,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
|
@ -4,7 +4,7 @@ import { IntegrationsSchema } from "@app/db/schemas";
|
|||||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
import { INTEGRATION } from "@app/lib/api-docs";
|
import { INTEGRATION } from "@app/lib/api-docs";
|
||||||
import { removeTrailingSlash, shake } from "@app/lib/fn";
|
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 { getTelemetryDistinctId } from "@app/server/lib/telemetry";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
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({
|
server.route({
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
url: "/:integrationId",
|
url: "/:integrationId",
|
||||||
|
@ -14,7 +14,7 @@ import { AUDIT_LOGS, ORGANIZATIONS } from "@app/lib/api-docs";
|
|||||||
import { getLastMidnightDateISO } from "@app/lib/fn";
|
import { getLastMidnightDateISO } from "@app/lib/fn";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
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) => {
|
export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
||||||
server.route({
|
server.route({
|
||||||
@ -74,8 +74,35 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
|||||||
schema: {
|
schema: {
|
||||||
description: "Get all audit logs for an organization",
|
description: "Get all audit logs for an organization",
|
||||||
querystring: z.object({
|
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),
|
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),
|
startDate: z.string().datetime().optional().describe(AUDIT_LOGS.EXPORT.startDate),
|
||||||
endDate: z.string().datetime().optional().describe(AUDIT_LOGS.EXPORT.endDate),
|
endDate: z.string().datetime().optional().describe(AUDIT_LOGS.EXPORT.endDate),
|
||||||
offset: z.coerce.number().default(0).describe(AUDIT_LOGS.EXPORT.offset),
|
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]),
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const auditLogs = await server.services.auditLog.listAuditLogs({
|
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,
|
actorId: req.permission.id,
|
||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
...req.query,
|
|
||||||
endDate: req.query.endDate,
|
|
||||||
startDate: req.query.startDate || getLastMidnightDateISO(),
|
|
||||||
auditLogActor: req.query.actor,
|
|
||||||
actor: req.permission.type
|
actor: req.permission.type
|
||||||
});
|
});
|
||||||
return { auditLogs };
|
return { auditLogs };
|
||||||
|
@ -34,6 +34,7 @@ export enum AuthMode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export enum ActorType { // would extend to AWS, Azure, ...
|
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
|
USER = "user", // userIdentity
|
||||||
SERVICE = "service",
|
SERVICE = "service",
|
||||||
IDENTITY = "identity",
|
IDENTITY = "identity",
|
||||||
|
@ -2,7 +2,7 @@ import { ForbiddenError, subject } from "@casl/ability";
|
|||||||
|
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
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 { TProjectPermission } from "@app/lib/types";
|
||||||
|
|
||||||
import { TIntegrationAuthDALFactory } from "../integration-auth/integration-auth-dal";
|
import { TIntegrationAuthDALFactory } from "../integration-auth/integration-auth-dal";
|
||||||
@ -19,6 +19,7 @@ import { TIntegrationDALFactory } from "./integration-dal";
|
|||||||
import {
|
import {
|
||||||
TCreateIntegrationDTO,
|
TCreateIntegrationDTO,
|
||||||
TDeleteIntegrationDTO,
|
TDeleteIntegrationDTO,
|
||||||
|
TGetIntegrationDTO,
|
||||||
TSyncIntegrationDTO,
|
TSyncIntegrationDTO,
|
||||||
TUpdateIntegrationDTO
|
TUpdateIntegrationDTO
|
||||||
} from "./integration-types";
|
} from "./integration-types";
|
||||||
@ -180,6 +181,27 @@ export const integrationServiceFactory = ({
|
|||||||
return updatedIntegration;
|
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 ({
|
const deleteIntegration = async ({
|
||||||
actorId,
|
actorId,
|
||||||
id,
|
id,
|
||||||
@ -276,6 +298,8 @@ export const integrationServiceFactory = ({
|
|||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
|
||||||
|
|
||||||
await secretQueueService.syncIntegrations({
|
await secretQueueService.syncIntegrations({
|
||||||
|
isManual: true,
|
||||||
|
actorId,
|
||||||
environment: integration.environment.slug,
|
environment: integration.environment.slug,
|
||||||
secretPath: integration.secretPath,
|
secretPath: integration.secretPath,
|
||||||
projectId: integration.projectId
|
projectId: integration.projectId
|
||||||
@ -289,6 +313,7 @@ export const integrationServiceFactory = ({
|
|||||||
updateIntegration,
|
updateIntegration,
|
||||||
deleteIntegration,
|
deleteIntegration,
|
||||||
listIntegrationByProject,
|
listIntegrationByProject,
|
||||||
|
getIntegration,
|
||||||
syncIntegration
|
syncIntegration
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -39,6 +39,10 @@ export type TCreateIntegrationDTO = {
|
|||||||
};
|
};
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TGetIntegrationDTO = {
|
||||||
|
id: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
export type TUpdateIntegrationDTO = {
|
export type TUpdateIntegrationDTO = {
|
||||||
id: string;
|
id: string;
|
||||||
app?: string;
|
app?: string;
|
||||||
|
@ -26,10 +26,7 @@ export const getBotKeyFnFactory = (
|
|||||||
) => {
|
) => {
|
||||||
const getBotKeyFn = async (projectId: string) => {
|
const getBotKeyFn = async (projectId: string) => {
|
||||||
const project = await projectDAL.findById(projectId);
|
const project = await projectDAL.findById(projectId);
|
||||||
if (!project)
|
if (!project) throw new BadRequestError({ message: "Project not found during bot lookup." });
|
||||||
throw new BadRequestError({
|
|
||||||
message: "Project not found during bot lookup. Are you sure you are using the correct project ID?"
|
|
||||||
});
|
|
||||||
|
|
||||||
if (project.version === 3) {
|
if (project.version === 3) {
|
||||||
return { project, shouldUseSecretV2Bridge: true };
|
return { project, shouldUseSecretV2Bridge: true };
|
||||||
|
@ -512,11 +512,7 @@ export const secretImportServiceFactory = ({
|
|||||||
return importedSecrets;
|
return importedSecrets;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!botKey)
|
if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
|
||||||
throw new BadRequestError({
|
|
||||||
message: "Project bot not found. Please upgrade your project.",
|
|
||||||
name: "bot_not_found_error"
|
|
||||||
});
|
|
||||||
|
|
||||||
const importedSecrets = await fnSecretsFromImports({ allowedImports, folderDAL, secretDAL, secretImportDAL });
|
const importedSecrets = await fnSecretsFromImports({ allowedImports, folderDAL, secretDAL, secretImportDAL });
|
||||||
return importedSecrets.map((el) => ({
|
return importedSecrets.map((el) => ({
|
||||||
|
@ -832,11 +832,7 @@ export const createManySecretsRawFnFactory = ({
|
|||||||
secretDAL
|
secretDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!botKey)
|
if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
|
||||||
throw new BadRequestError({
|
|
||||||
message: "Project bot not found. Please upgrade your project.",
|
|
||||||
name: "bot_not_found_error"
|
|
||||||
});
|
|
||||||
const inputSecrets = secrets.map((secret) => {
|
const inputSecrets = secrets.map((secret) => {
|
||||||
const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(secret.secretName, botKey);
|
const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(secret.secretName, botKey);
|
||||||
const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(secret.secretValue || "", botKey);
|
const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(secret.secretValue || "", botKey);
|
||||||
@ -997,11 +993,7 @@ export const updateManySecretsRawFnFactory = ({
|
|||||||
return updatedSecrets;
|
return updatedSecrets;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!botKey)
|
if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
|
||||||
throw new BadRequestError({
|
|
||||||
message: "Project bot not found. Please upgrade your project.",
|
|
||||||
name: "bot_not_found_error"
|
|
||||||
});
|
|
||||||
const blindIndexCfg = await secretBlindIndexDAL.findOne({ projectId });
|
const blindIndexCfg = await secretBlindIndexDAL.findOne({ projectId });
|
||||||
if (!blindIndexCfg) throw new BadRequestError({ message: "Blind index not found", name: "Update secret" });
|
if (!blindIndexCfg) throw new BadRequestError({ message: "Blind index not found", name: "Update secret" });
|
||||||
|
|
||||||
|
@ -2,6 +2,8 @@
|
|||||||
import { AxiosError } from "axios";
|
import { AxiosError } from "axios";
|
||||||
|
|
||||||
import { ProjectUpgradeStatus, ProjectVersion, TSecretSnapshotSecretsV2, TSecretVersionsV2 } from "@app/db/schemas";
|
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 { TSecretApprovalRequestDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-dal";
|
||||||
import { TSecretRotationDALFactory } from "@app/ee/services/secret-rotation/secret-rotation-dal";
|
import { TSecretRotationDALFactory } from "@app/ee/services/secret-rotation/secret-rotation-dal";
|
||||||
import { TSnapshotDALFactory } from "@app/ee/services/secret-snapshot/snapshot-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 { TSecretBlindIndexDALFactory } from "@app/services/secret-blind-index/secret-blind-index-dal";
|
||||||
import { TSecretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal";
|
import { TSecretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal";
|
||||||
|
|
||||||
|
import { ActorType } from "../auth/auth-type";
|
||||||
import { TIntegrationDALFactory } from "../integration/integration-dal";
|
import { TIntegrationDALFactory } from "../integration/integration-dal";
|
||||||
import { TIntegrationAuthDALFactory } from "../integration-auth/integration-auth-dal";
|
import { TIntegrationAuthDALFactory } from "../integration-auth/integration-auth-dal";
|
||||||
import { TIntegrationAuthServiceFactory } from "../integration-auth/integration-auth-service";
|
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 { TSecretVersionV2DALFactory } from "../secret-v2-bridge/secret-version-dal";
|
||||||
import { TSecretVersionV2TagDALFactory } from "../secret-v2-bridge/secret-version-tag-dal";
|
import { TSecretVersionV2TagDALFactory } from "../secret-v2-bridge/secret-version-tag-dal";
|
||||||
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
||||||
|
import { TUserDALFactory } from "../user/user-dal";
|
||||||
import { TWebhookDALFactory } from "../webhook/webhook-dal";
|
import { TWebhookDALFactory } from "../webhook/webhook-dal";
|
||||||
import { fnTriggerWebhook } from "../webhook/webhook-fns";
|
import { fnTriggerWebhook } from "../webhook/webhook-fns";
|
||||||
import { TSecretDALFactory } from "./secret-dal";
|
import { TSecretDALFactory } from "./secret-dal";
|
||||||
@ -71,6 +75,7 @@ type TSecretQueueFactoryDep = {
|
|||||||
secretVersionDAL: TSecretVersionDALFactory;
|
secretVersionDAL: TSecretVersionDALFactory;
|
||||||
secretBlindIndexDAL: TSecretBlindIndexDALFactory;
|
secretBlindIndexDAL: TSecretBlindIndexDALFactory;
|
||||||
secretTagDAL: TSecretTagDALFactory;
|
secretTagDAL: TSecretTagDALFactory;
|
||||||
|
userDAL: Pick<TUserDALFactory, "findById">;
|
||||||
secretVersionTagDAL: TSecretVersionTagDALFactory;
|
secretVersionTagDAL: TSecretVersionTagDALFactory;
|
||||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||||
secretV2BridgeDAL: TSecretV2BridgeDALFactory;
|
secretV2BridgeDAL: TSecretV2BridgeDALFactory;
|
||||||
@ -81,6 +86,7 @@ type TSecretQueueFactoryDep = {
|
|||||||
snapshotDAL: Pick<TSnapshotDALFactory, "findNSecretV1SnapshotByFolderId" | "deleteSnapshotsAboveLimit">;
|
snapshotDAL: Pick<TSnapshotDALFactory, "findNSecretV1SnapshotByFolderId" | "deleteSnapshotsAboveLimit">;
|
||||||
snapshotSecretV2BridgeDAL: Pick<TSnapshotSecretV2DALFactory, "insertMany" | "batchInsert">;
|
snapshotSecretV2BridgeDAL: Pick<TSnapshotSecretV2DALFactory, "insertMany" | "batchInsert">;
|
||||||
keyStore: Pick<TKeyStoreFactory, "acquireLock" | "setItemWithExpiry" | "getItem">;
|
keyStore: Pick<TKeyStoreFactory, "acquireLock" | "setItemWithExpiry" | "getItem">;
|
||||||
|
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TGetSecrets = {
|
export type TGetSecrets = {
|
||||||
@ -106,6 +112,7 @@ export const secretQueueFactory = ({
|
|||||||
secretDAL,
|
secretDAL,
|
||||||
secretImportDAL,
|
secretImportDAL,
|
||||||
folderDAL,
|
folderDAL,
|
||||||
|
userDAL,
|
||||||
webhookDAL,
|
webhookDAL,
|
||||||
projectEnvDAL,
|
projectEnvDAL,
|
||||||
orgDAL,
|
orgDAL,
|
||||||
@ -125,7 +132,8 @@ export const secretQueueFactory = ({
|
|||||||
snapshotDAL,
|
snapshotDAL,
|
||||||
snapshotSecretV2BridgeDAL,
|
snapshotSecretV2BridgeDAL,
|
||||||
secretApprovalRequestDAL,
|
secretApprovalRequestDAL,
|
||||||
keyStore
|
keyStore,
|
||||||
|
auditLogService
|
||||||
}: TSecretQueueFactoryDep) => {
|
}: TSecretQueueFactoryDep) => {
|
||||||
const removeSecretReminder = async (dto: TRemoveSecretReminderDTO) => {
|
const removeSecretReminder = async (dto: TRemoveSecretReminderDTO) => {
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
@ -430,7 +438,9 @@ export const secretQueueFactory = ({
|
|||||||
return content;
|
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, {
|
await queueService.queue(QueueName.IntegrationSync, QueueJobs.IntegrationSync, dto, {
|
||||||
attempts: 3,
|
attempts: 3,
|
||||||
delay: 1000,
|
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) {
|
if (!excludeReplication) {
|
||||||
await replicateSecrets({
|
await replicateSecrets({
|
||||||
_deDupeReplicationQueue: deDupeReplicationQueue,
|
_deDupeReplicationQueue: deDupeReplicationQueue,
|
||||||
@ -544,7 +554,7 @@ export const secretQueueFactory = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
queueService.start(QueueName.IntegrationSync, async (job) => {
|
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;
|
if (depth > MAX_SYNC_SECRET_DEPTH) return;
|
||||||
|
|
||||||
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
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
|
// akhilmhdh: this try catch is for lock release
|
||||||
try {
|
try {
|
||||||
const secrets = shouldUseSecretV2Bridge
|
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, {
|
await integrationDAL.updateById(integration.id, {
|
||||||
lastSyncJobId: job.id,
|
lastSyncJobId: job.id,
|
||||||
lastUsed: new Date(),
|
lastUsed: new Date(),
|
||||||
@ -794,9 +843,23 @@ export const secretQueueFactory = ({
|
|||||||
(err instanceof AxiosError ? JSON.stringify(err?.response?.data) : (err as Error)?.message) ||
|
(err instanceof AxiosError ? JSON.stringify(err?.response?.data) : (err as Error)?.message) ||
|
||||||
"Unknown error occurred.";
|
"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, {
|
await integrationDAL.updateById(integration.id, {
|
||||||
lastSyncJobId: job.id,
|
lastSyncJobId: job.id,
|
||||||
lastUsed: new Date(),
|
|
||||||
syncMessage: message,
|
syncMessage: message,
|
||||||
isSynced: false
|
isSynced: false
|
||||||
});
|
});
|
||||||
|
@ -985,11 +985,7 @@ export const secretServiceFactory = ({
|
|||||||
return { secrets, imports };
|
return { secrets, imports };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!botKey)
|
if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
|
||||||
throw new BadRequestError({
|
|
||||||
message: "Project bot not found. Please upgrade your project.",
|
|
||||||
name: "bot_not_found_error"
|
|
||||||
});
|
|
||||||
|
|
||||||
const { secrets, imports } = await getSecrets({
|
const { secrets, imports } = await getSecrets({
|
||||||
actorId,
|
actorId,
|
||||||
@ -1150,10 +1146,7 @@ export const secretServiceFactory = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!botKey)
|
if (!botKey)
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({ message: "Please upgrade your project first", name: "bot_not_found_error" });
|
||||||
message: "Project bot not found. Please upgrade your project.",
|
|
||||||
name: "bot_not_found_error"
|
|
||||||
});
|
|
||||||
const decryptedSecret = decryptSecretRaw(encryptedSecret, botKey);
|
const decryptedSecret = decryptSecretRaw(encryptedSecret, botKey);
|
||||||
|
|
||||||
if (expandSecretReferences) {
|
if (expandSecretReferences) {
|
||||||
@ -1245,11 +1238,7 @@ export const secretServiceFactory = ({
|
|||||||
return { secret, type: SecretProtectionType.Direct as const };
|
return { secret, type: SecretProtectionType.Direct as const };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!botKey)
|
if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
|
||||||
throw new BadRequestError({
|
|
||||||
message: "Project bot not found. Please upgrade your project.",
|
|
||||||
name: "bot_not_found_error"
|
|
||||||
});
|
|
||||||
const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(secretName, botKey);
|
const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(secretName, botKey);
|
||||||
const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(secretValue || "", botKey);
|
const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(secretValue || "", botKey);
|
||||||
const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8(secretComment || "", botKey);
|
const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8(secretComment || "", botKey);
|
||||||
@ -1387,11 +1376,7 @@ export const secretServiceFactory = ({
|
|||||||
return { type: SecretProtectionType.Direct as const, secret };
|
return { type: SecretProtectionType.Direct as const, secret };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!botKey)
|
if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
|
||||||
throw new BadRequestError({
|
|
||||||
message: "Project bot not found. Please upgrade your project.",
|
|
||||||
name: "bot_not_found_error"
|
|
||||||
});
|
|
||||||
|
|
||||||
const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(secretValue || "", botKey);
|
const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(secretValue || "", botKey);
|
||||||
const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8(secretComment || "", botKey);
|
const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8(secretComment || "", botKey);
|
||||||
@ -1513,11 +1498,7 @@ export const secretServiceFactory = ({
|
|||||||
});
|
});
|
||||||
return { type: SecretProtectionType.Direct as const, secret };
|
return { type: SecretProtectionType.Direct as const, secret };
|
||||||
}
|
}
|
||||||
if (!botKey)
|
if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
|
||||||
throw new BadRequestError({
|
|
||||||
message: "Project bot not found. Please upgrade your project.",
|
|
||||||
name: "bot_not_found_error"
|
|
||||||
});
|
|
||||||
if (policy) {
|
if (policy) {
|
||||||
const approval = await secretApprovalRequestService.generateSecretApprovalRequest({
|
const approval = await secretApprovalRequestService.generateSecretApprovalRequest({
|
||||||
policy,
|
policy,
|
||||||
@ -1617,11 +1598,7 @@ export const secretServiceFactory = ({
|
|||||||
return { secrets, type: SecretProtectionType.Direct as const };
|
return { secrets, type: SecretProtectionType.Direct as const };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!botKey)
|
if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
|
||||||
throw new BadRequestError({
|
|
||||||
message: "Project bot not found. Please upgrade your project.",
|
|
||||||
name: "bot_not_found_error"
|
|
||||||
});
|
|
||||||
const sanitizedSecrets = inputSecrets.map(
|
const sanitizedSecrets = inputSecrets.map(
|
||||||
({ secretComment, secretKey, metadata, tagIds, secretValue, skipMultilineEncoding }) => {
|
({ secretComment, secretKey, metadata, tagIds, secretValue, skipMultilineEncoding }) => {
|
||||||
const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(secretKey, botKey);
|
const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(secretKey, botKey);
|
||||||
@ -1743,11 +1720,7 @@ export const secretServiceFactory = ({
|
|||||||
return { type: SecretProtectionType.Direct as const, secrets };
|
return { type: SecretProtectionType.Direct as const, secrets };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!botKey)
|
if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
|
||||||
throw new BadRequestError({
|
|
||||||
message: "Project bot not found. Please upgrade your project.",
|
|
||||||
name: "bot_not_found_error"
|
|
||||||
});
|
|
||||||
const sanitizedSecrets = inputSecrets.map(
|
const sanitizedSecrets = inputSecrets.map(
|
||||||
({
|
({
|
||||||
secretComment,
|
secretComment,
|
||||||
@ -1875,11 +1848,7 @@ export const secretServiceFactory = ({
|
|||||||
return { type: SecretProtectionType.Direct as const, secrets };
|
return { type: SecretProtectionType.Direct as const, secrets };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!botKey)
|
if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
|
||||||
throw new BadRequestError({
|
|
||||||
message: "Project bot not found. Please upgrade your project.",
|
|
||||||
name: "bot_not_found_error"
|
|
||||||
});
|
|
||||||
|
|
||||||
if (policy) {
|
if (policy) {
|
||||||
const approval = await secretApprovalRequestService.generateSecretApprovalRequest({
|
const approval = await secretApprovalRequestService.generateSecretApprovalRequest({
|
||||||
@ -2213,10 +2182,7 @@ export const secretServiceFactory = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!botKey)
|
if (!botKey)
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({ message: "Please upgrade your project first", name: "bot_not_found_error" });
|
||||||
message: "Project bot not found. Please upgrade your project.",
|
|
||||||
name: "bot_not_found_error"
|
|
||||||
});
|
|
||||||
|
|
||||||
await secretDAL.transaction(async (tx) => {
|
await secretDAL.transaction(async (tx) => {
|
||||||
const secrets = await secretDAL.findAllProjectSecretValues(projectId, tx);
|
const secrets = await secretDAL.findAllProjectSecretValues(projectId, tx);
|
||||||
@ -2299,10 +2265,7 @@ export const secretServiceFactory = ({
|
|||||||
|
|
||||||
const { botKey } = await projectBotService.getBotKey(project.id);
|
const { botKey } = await projectBotService.getBotKey(project.id);
|
||||||
if (!botKey) {
|
if (!botKey) {
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
|
||||||
message: "Project bot not found. Please upgrade your project.",
|
|
||||||
name: "bot_not_found_error"
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const sourceFolder = await folderDAL.findBySecretPath(project.id, sourceEnvironment, sourceSecretPath);
|
const sourceFolder = await folderDAL.findBySecretPath(project.id, sourceEnvironment, sourceSecretPath);
|
||||||
|
@ -4,7 +4,6 @@ Copyright (c) 2023 Infisical Inc.
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@ -44,26 +43,14 @@ func init() {
|
|||||||
rootCmd.PersistentFlags().Bool("silent", false, "Disable output of tip/info messages. Useful when running in scripts or CI/CD pipelines.")
|
rootCmd.PersistentFlags().Bool("silent", false, "Disable output of tip/info messages. Useful when running in scripts or CI/CD pipelines.")
|
||||||
rootCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
|
rootCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
|
||||||
silent, err := cmd.Flags().GetBool("silent")
|
silent, err := cmd.Flags().GetBool("silent")
|
||||||
|
config.INFISICAL_URL = util.AppendAPIEndpoint(config.INFISICAL_URL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.HandleError(err)
|
util.HandleError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
config.INFISICAL_URL = util.AppendAPIEndpoint(config.INFISICAL_URL)
|
|
||||||
|
|
||||||
if !util.IsRunningInDocker() && !silent {
|
if !util.IsRunningInDocker() && !silent {
|
||||||
util.CheckForUpdate()
|
util.CheckForUpdate()
|
||||||
}
|
}
|
||||||
|
|
||||||
loggedInDetails, err := util.GetCurrentLoggedInUserDetails()
|
|
||||||
|
|
||||||
if !silent && err == nil && loggedInDetails.IsUserLoggedIn && !loggedInDetails.LoginExpired {
|
|
||||||
token, err := util.GetInfisicalToken(cmd)
|
|
||||||
|
|
||||||
if err == nil && token != nil {
|
|
||||||
util.PrintWarning(fmt.Sprintf("Your logged-in session is being overwritten by the token provided from the %s.", token.Source))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// if config.INFISICAL_URL is set to the default value, check if INFISICAL_URL is set in the environment
|
// if config.INFISICAL_URL is set to the default value, check if INFISICAL_URL is set in the environment
|
||||||
|
@ -160,19 +160,19 @@ var secretsSetCmd = &cobra.Command{
|
|||||||
util.HandleError(err, "Unable to parse flag")
|
util.HandleError(err, "Unable to parse flag")
|
||||||
}
|
}
|
||||||
|
|
||||||
if token == nil {
|
if (token == nil) {
|
||||||
util.RequireLocalWorkspaceFile()
|
util.RequireLocalWorkspaceFile()
|
||||||
}
|
}
|
||||||
|
|
||||||
environmentName, _ := cmd.Flags().GetString("env")
|
environmentName, _ := cmd.Flags().GetString("env")
|
||||||
if !cmd.Flags().Changed("env") {
|
if !cmd.Flags().Changed("env") {
|
||||||
environmentFromWorkspace := util.GetEnvFromWorkspaceFile()
|
environmentFromWorkspace := util.GetEnvFromWorkspaceFile()
|
||||||
if environmentFromWorkspace != "" {
|
if environmentFromWorkspace != "" {
|
||||||
environmentName = environmentFromWorkspace
|
environmentName = environmentFromWorkspace
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
projectId, err := cmd.Flags().GetString("projectId")
|
projectId, err := cmd.Flags().GetString("projectId")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.HandleError(err, "Unable to parse flag")
|
util.HandleError(err, "Unable to parse flag")
|
||||||
}
|
}
|
||||||
|
@ -63,9 +63,8 @@ type DynamicSecretLease struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type TokenDetails struct {
|
type TokenDetails struct {
|
||||||
Type string
|
Type string
|
||||||
Token string
|
Token string
|
||||||
Source string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type SingleFolder struct {
|
type SingleFolder struct {
|
||||||
|
@ -87,15 +87,11 @@ func GetInfisicalToken(cmd *cobra.Command) (token *models.TokenDetails, err erro
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var source = "--token flag"
|
|
||||||
|
|
||||||
if infisicalToken == "" { // If no flag is passed, we first check for the universal auth access token env variable.
|
if infisicalToken == "" { // If no flag is passed, we first check for the universal auth access token env variable.
|
||||||
infisicalToken = os.Getenv(INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN_NAME)
|
infisicalToken = os.Getenv(INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN_NAME)
|
||||||
source = fmt.Sprintf("%s environment variable", INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN_NAME)
|
|
||||||
|
|
||||||
if infisicalToken == "" { // If it's still empty after the first env check, we check for the service token env variable.
|
if infisicalToken == "" { // If it's still empty after the first env check, we check for the service token env variable.
|
||||||
infisicalToken = os.Getenv(INFISICAL_TOKEN_NAME)
|
infisicalToken = os.Getenv(INFISICAL_TOKEN_NAME)
|
||||||
source = fmt.Sprintf("%s environment variable", INFISICAL_TOKEN_NAME)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -105,16 +101,14 @@ func GetInfisicalToken(cmd *cobra.Command) (token *models.TokenDetails, err erro
|
|||||||
|
|
||||||
if strings.HasPrefix(infisicalToken, "st.") {
|
if strings.HasPrefix(infisicalToken, "st.") {
|
||||||
return &models.TokenDetails{
|
return &models.TokenDetails{
|
||||||
Type: SERVICE_TOKEN_IDENTIFIER,
|
Type: SERVICE_TOKEN_IDENTIFIER,
|
||||||
Token: infisicalToken,
|
Token: infisicalToken,
|
||||||
Source: source,
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return &models.TokenDetails{
|
return &models.TokenDetails{
|
||||||
Type: UNIVERSAL_AUTH_TOKEN_IDENTIFIER,
|
Type: UNIVERSAL_AUTH_TOKEN_IDENTIFIER,
|
||||||
Token: infisicalToken,
|
Token: infisicalToken,
|
||||||
Source: source,
|
|
||||||
}, nil
|
}, nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,21 @@ export const UserProvider = ({ children }: Props): JSX.Element => {
|
|||||||
};
|
};
|
||||||
}, [data, isLoading]);
|
}, [data, isLoading]);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="flex h-screen w-screen items-center justify-center bg-bunker-800">
|
||||||
|
<img
|
||||||
|
src="/images/loading/loading.gif"
|
||||||
|
height={70}
|
||||||
|
width={120}
|
||||||
|
decoding="async"
|
||||||
|
loading="lazy"
|
||||||
|
alt="infisical loading indicator"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return <UserContext.Provider value={value}>{children}</UserContext.Provider>;
|
return <UserContext.Provider value={value}>{children}</UserContext.Provider>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -79,7 +79,8 @@ export const eventToNameMap: { [K in EventType]: string } = {
|
|||||||
[EventType.UPDATE_CERTIFICATE_TEMPLATE_EST_CONFIG]:
|
[EventType.UPDATE_CERTIFICATE_TEMPLATE_EST_CONFIG]:
|
||||||
"Update certificate template EST configuration",
|
"Update certificate template EST configuration",
|
||||||
[EventType.UPDATE_PROJECT_SLACK_CONFIG]: "Update project slack 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 } = {
|
export const userAgentTTypeoNameMap: { [K in UserAgentType]: string } = {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
export enum ActorType {
|
export enum ActorType {
|
||||||
|
PLATFORM = "platform",
|
||||||
USER = "user",
|
USER = "user",
|
||||||
SERVICE = "service",
|
SERVICE = "service",
|
||||||
IDENTITY = "identity"
|
IDENTITY = "identity"
|
||||||
@ -91,5 +92,6 @@ export enum EventType {
|
|||||||
UPDATE_CERTIFICATE_TEMPLATE_EST_CONFIG = "update-certificate-template-est-config",
|
UPDATE_CERTIFICATE_TEMPLATE_EST_CONFIG = "update-certificate-template-est-config",
|
||||||
GET_CERTIFICATE_TEMPLATE_EST_CONFIG = "get-certificate-template-est-config",
|
GET_CERTIFICATE_TEMPLATE_EST_CONFIG = "get-certificate-template-est-config",
|
||||||
UPDATE_PROJECT_SLACK_CONFIG = "update-project-slack-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"
|
||||||
}
|
}
|
||||||
|
@ -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 { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
import { Actor, AuditLog, AuditLogFilters } from "./types";
|
import { Actor, AuditLog, TGetAuditLogsFilter } from "./types";
|
||||||
|
|
||||||
export const auditLogKeys = {
|
export const auditLogKeys = {
|
||||||
getAuditLogs: (workspaceId: string | null, filters: AuditLogFilters) =>
|
getAuditLogs: (workspaceId: string | null, filters: TGetAuditLogsFilter) =>
|
||||||
[{ workspaceId, filters }, "audit-logs"] as const,
|
[{ workspaceId, filters }, "audit-logs"] as const,
|
||||||
getAuditLogActorFilterOpts: (workspaceId: string) =>
|
getAuditLogActorFilterOpts: (workspaceId: string) =>
|
||||||
[{ workspaceId }, "audit-log-actor-filters"] as const
|
[{ 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({
|
return useInfiniteQuery({
|
||||||
queryKey: auditLogKeys.getAuditLogs(workspaceId, filters),
|
queryKey: auditLogKeys.getAuditLogs(projectId, filters),
|
||||||
queryFn: async ({ pageParam }) => {
|
queryFn: async ({ pageParam }) => {
|
||||||
const auditLogEndpoint = workspaceId
|
const { data } = await apiRequest.get<{ auditLogs: AuditLog[] }>(
|
||||||
? `/api/v1/workspace/${workspaceId}/audit-logs`
|
"/api/v1/organization/audit-logs",
|
||||||
: "/api/v1/organization/audit-logs";
|
{
|
||||||
const { data } = await apiRequest.get<{ auditLogs: AuditLog[] }>(auditLogEndpoint, {
|
params: {
|
||||||
params: {
|
...filters,
|
||||||
...filters,
|
offset: pageParam,
|
||||||
offset: pageParam,
|
startDate: filters?.startDate?.toISOString(),
|
||||||
startDate: filters?.startDate?.toISOString(),
|
endDate: filters?.endDate?.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;
|
return data.auditLogs;
|
||||||
},
|
},
|
||||||
getNextPageParam: (lastPage, pages) =>
|
getNextPageParam: (lastPage, pages) =>
|
||||||
lastPage.length !== 0 ? pages.length * filters.limit : undefined
|
lastPage.length !== 0 ? pages.length * filters.limit : undefined,
|
||||||
|
...options
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -3,6 +3,17 @@ import { IdentityTrustedIp } from "../identities/types";
|
|||||||
import { PkiItemType } from "../pkiCollections/constants";
|
import { PkiItemType } from "../pkiCollections/constants";
|
||||||
import { ActorType, EventType, UserAgentType } from "./enums";
|
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 {
|
interface UserActorMetadata {
|
||||||
userId: string;
|
userId: string;
|
||||||
email: string;
|
email: string;
|
||||||
@ -33,7 +44,13 @@ export interface IdentityActor {
|
|||||||
metadata: IdentityActorMetadata;
|
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 {
|
interface GetSecretsEvent {
|
||||||
type: EventType.GET_SECRETS;
|
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 =
|
export type Event =
|
||||||
| GetSecretsEvent
|
| GetSecretsEvent
|
||||||
| GetSecretEvent
|
| GetSecretEvent
|
||||||
@ -838,7 +871,8 @@ export type Event =
|
|||||||
| CreateCertificateTemplateEstConfig
|
| CreateCertificateTemplateEstConfig
|
||||||
| GetCertificateTemplateEstConfig
|
| GetCertificateTemplateEstConfig
|
||||||
| UpdateProjectSlackConfig
|
| UpdateProjectSlackConfig
|
||||||
| GetProjectSlackConfig;
|
| GetProjectSlackConfig
|
||||||
|
| IntegrationSyncedEvent;
|
||||||
|
|
||||||
export type AuditLog = {
|
export type AuditLog = {
|
||||||
id: string;
|
id: string;
|
||||||
@ -856,12 +890,3 @@ export type AuditLog = {
|
|||||||
slug: string;
|
slug: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AuditLogFilters = {
|
|
||||||
eventType?: EventType;
|
|
||||||
userAgentType?: UserAgentType;
|
|
||||||
actor?: string;
|
|
||||||
limit: number;
|
|
||||||
startDate?: Date;
|
|
||||||
endDate?: Date;
|
|
||||||
};
|
|
||||||
|
@ -1 +1,6 @@
|
|||||||
export { useCreateIntegration, useDeleteIntegration, useGetCloudIntegrations } from "./queries";
|
export {
|
||||||
|
useCreateIntegration,
|
||||||
|
useDeleteIntegration,
|
||||||
|
useGetCloudIntegrations,
|
||||||
|
useGetIntegration
|
||||||
|
} from "./queries";
|
||||||
|
@ -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 { createNotification } from "@app/components/notifications";
|
||||||
import { apiRequest } from "@app/config/request";
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
import { workspaceKeys } from "../workspace";
|
import { workspaceKeys } from "../workspace";
|
||||||
import { TCloudIntegration } from "./types";
|
import { TCloudIntegration, TIntegrationWithEnv } from "./types";
|
||||||
|
|
||||||
export const integrationQueryKeys = {
|
export const integrationQueryKeys = {
|
||||||
getIntegrations: () => ["integrations"] as const
|
getIntegrations: () => ["integrations"] as const,
|
||||||
|
getIntegration: (id: string) => ["integration", id] as const
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchIntegrations = async () => {
|
const fetchIntegrations = async () => {
|
||||||
@ -18,6 +19,14 @@ const fetchIntegrations = async () => {
|
|||||||
return data.integrationOptions;
|
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 = () =>
|
export const useGetCloudIntegrations = () =>
|
||||||
useQuery({
|
useQuery({
|
||||||
queryKey: integrationQueryKeys.getIntegrations(),
|
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 = () => {
|
export const useSyncIntegration = () => {
|
||||||
return useMutation<{}, {}, { id: string; workspaceId: string; lastUsed: string }>({
|
return useMutation<{}, {}, { id: string; workspaceId: string; lastUsed: string }>({
|
||||||
mutationFn: ({ id }) => apiRequest.post(`/api/v1/integration/${id}/sync`),
|
mutationFn: ({ id }) => apiRequest.post(`/api/v1/integration/${id}/sync`),
|
||||||
|
@ -36,14 +36,34 @@ export type TIntegration = {
|
|||||||
metadata?: {
|
metadata?: {
|
||||||
githubVisibility?: string;
|
githubVisibility?: string;
|
||||||
githubVisibilityRepoIds?: string[];
|
githubVisibilityRepoIds?: string[];
|
||||||
|
shouldAutoRedeploy?: boolean;
|
||||||
|
secretAWSTag?: {
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
}[];
|
||||||
|
|
||||||
|
kmsKeyId?: string;
|
||||||
secretSuffix?: string;
|
secretSuffix?: string;
|
||||||
|
secretPrefix?: string;
|
||||||
syncBehavior?: IntegrationSyncBehavior;
|
syncBehavior?: IntegrationSyncBehavior;
|
||||||
mappingBehavior?: IntegrationMappingBehavior;
|
mappingBehavior?: IntegrationMappingBehavior;
|
||||||
scope: string;
|
scope: string;
|
||||||
org: string;
|
org: string;
|
||||||
project: string;
|
project: string;
|
||||||
environment: string;
|
environment: string;
|
||||||
|
|
||||||
|
shouldDisableDelete?: boolean;
|
||||||
|
shouldMaskSecrets?: boolean;
|
||||||
|
shouldProtectSecrets?: boolean;
|
||||||
|
shouldEnableDelete?: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TIntegrationWithEnv = TIntegration & {
|
||||||
|
environment: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
23
frontend/src/pages/integrations/details/[integrationId].tsx
Normal file
23
frontend/src/pages/integrations/details/[integrationId].tsx
Normal 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;
|
@ -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;
|
||||||
|
};
|
@ -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;
|
||||||
|
};
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1 @@
|
|||||||
|
export { IntegrationDetailsPage } from "./IntegrationDetailsPage";
|
@ -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 {
|
import {
|
||||||
faArrowRight,
|
faArrowRight,
|
||||||
faCalendarCheck,
|
faCalendarCheck,
|
||||||
|
faEllipsis,
|
||||||
faRefresh,
|
faRefresh,
|
||||||
faWarning,
|
faWarning,
|
||||||
faXmark
|
faXmark
|
||||||
@ -10,7 +14,7 @@ import { format } from "date-fns";
|
|||||||
import { integrationSlugNameMapping } from "public/data/frequentConstants";
|
import { integrationSlugNameMapping } from "public/data/frequentConstants";
|
||||||
|
|
||||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
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 { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||||
import { IntegrationMappingBehavior } from "@app/hooks/api/integrations/types";
|
import { IntegrationMappingBehavior } from "@app/hooks/api/integrations/types";
|
||||||
import { TIntegration } from "@app/hooks/api/types";
|
import { TIntegration } from "@app/hooks/api/types";
|
||||||
@ -28,9 +32,12 @@ export const ConfiguredIntegrationItem = ({
|
|||||||
onRemoveIntegration,
|
onRemoveIntegration,
|
||||||
onManualSyncIntegration
|
onManualSyncIntegration
|
||||||
}: IProps) => {
|
}: IProps) => {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<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()}`}
|
key={`integration-${integration?.id.toString()}`}
|
||||||
>
|
>
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
@ -168,9 +175,9 @@ export const ConfiguredIntegrationItem = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</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 && (
|
{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
|
<Tooltip
|
||||||
center
|
center
|
||||||
className="max-w-xs whitespace-normal break-words"
|
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 max-h-[10rem] flex-col overflow-auto ">
|
||||||
<div className="flex self-start">
|
<div className="flex self-start">
|
||||||
<FontAwesomeIcon icon={faCalendarCheck} className="pt-0.5 pr-2 text-sm" />
|
<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>
|
||||||
<div className="pl-5 text-left text-xs">
|
<div className="pl-5 text-left text-xs">
|
||||||
{format(new Date(integration.lastUsed), "yyyy-MM-dd, hh:mm aaa")}
|
{format(new Date(integration.lastUsed), "yyyy-MM-dd, hh:mm aaa")}
|
||||||
@ -195,43 +202,62 @@ export const ConfiguredIntegrationItem = ({
|
|||||||
</div>
|
</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>
|
<div>{integration.isSynced ? "Synced" : "Not synced"}</div>
|
||||||
{!integration.isSynced && <FontAwesomeIcon icon={faWarning} />}
|
{!integration.isSynced && <FontAwesomeIcon icon={faWarning} />}
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</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">
|
<Tooltip className="text-center" content="Manually sync integration secrets">
|
||||||
<Button
|
<IconButton
|
||||||
onClick={() => onManualSyncIntegration()}
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onManualSyncIntegration();
|
||||||
|
}}
|
||||||
|
ariaLabel="sync"
|
||||||
|
colorSchema="primary"
|
||||||
|
variant="star"
|
||||||
className="max-w-[2.5rem] border-none bg-mineshaft-500"
|
className="max-w-[2.5rem] border-none bg-mineshaft-500"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faRefresh} className="px-1 text-bunker-200" />
|
<FontAwesomeIcon icon={faRefresh} className="px-1" />
|
||||||
</Button>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
<ProjectPermissionCan
|
||||||
<ProjectPermissionCan
|
I={ProjectPermissionActions.Delete}
|
||||||
I={ProjectPermissionActions.Delete}
|
a={ProjectPermissionSub.Integrations}
|
||||||
a={ProjectPermissionSub.Integrations}
|
>
|
||||||
>
|
{(isAllowed: boolean) => (
|
||||||
{(isAllowed: boolean) => (
|
|
||||||
<div className="flex items-end opacity-80 duration-200 hover:opacity-100">
|
|
||||||
<Tooltip content="Remove Integration">
|
<Tooltip content="Remove Integration">
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={() => onRemoveIntegration()}
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onRemoveIntegration();
|
||||||
|
}}
|
||||||
ariaLabel="delete"
|
ariaLabel="delete"
|
||||||
isDisabled={!isAllowed}
|
isDisabled={!isAllowed}
|
||||||
colorSchema="danger"
|
colorSchema="danger"
|
||||||
variant="star"
|
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>
|
</IconButton>
|
||||||
</Tooltip>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -42,7 +42,9 @@ export const UserAuditLogsSection = withPermission(
|
|||||||
<LogsSection
|
<LogsSection
|
||||||
showFilters={showFilter}
|
showFilters={showFilter}
|
||||||
filterClassName="bg-mineshaft-900 static"
|
filterClassName="bg-mineshaft-900 static"
|
||||||
presetActor={orgMembership.user.id}
|
presets={{
|
||||||
|
actorId: orgMembership.user.id
|
||||||
|
}}
|
||||||
isOrgAuditLogs
|
isOrgAuditLogs
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,14 +1,29 @@
|
|||||||
|
/* eslint-disable no-nested-ternary */
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Control, Controller, UseFormReset } from "react-hook-form";
|
import { Control, Controller, UseFormReset, UseFormWatch } from "react-hook-form";
|
||||||
import { faFilterCircleXmark } from "@fortawesome/free-solid-svg-icons";
|
import {
|
||||||
|
faCheckCircle,
|
||||||
|
faChevronDown,
|
||||||
|
faFilterCircleXmark
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { twMerge } from "tailwind-merge";
|
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 { useWorkspace } from "@app/context";
|
||||||
import { useGetAuditLogActorFilterOpts } from "@app/hooks/api";
|
import { useGetAuditLogActorFilterOpts } from "@app/hooks/api";
|
||||||
import { eventToNameMap, userAgentTTypeoNameMap } from "@app/hooks/api/auditLogs/constants";
|
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 { Actor } from "@app/hooks/api/auditLogs/types";
|
||||||
|
|
||||||
import { AuditLogFilterFormData } from "./types";
|
import { AuditLogFilterFormData } from "./types";
|
||||||
@ -20,13 +35,17 @@ const userAgentTypes = Object.entries(userAgentTTypeoNameMap).map(([value, label
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
presetActor?: string;
|
presets?: {
|
||||||
|
actorId?: string;
|
||||||
|
eventType?: EventType[];
|
||||||
|
};
|
||||||
className?: string;
|
className?: string;
|
||||||
control: Control<AuditLogFilterFormData>;
|
control: Control<AuditLogFilterFormData>;
|
||||||
reset: UseFormReset<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 [isStartDatePickerOpen, setIsStartDatePickerOpen] = useState(false);
|
||||||
const [isEndDatePickerOpen, setIsEndDatePickerOpen] = 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 (
|
return (
|
||||||
<div
|
<div
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
@ -82,29 +103,69 @@ export const LogsFilter = ({ presetActor, className, control, reset }: Props) =>
|
|||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="eventType"
|
name="eventType"
|
||||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
render={({ field }) => (
|
||||||
<FormControl
|
<FormControl label="Events">
|
||||||
label="Event"
|
<DropdownMenu>
|
||||||
errorText={error?.message}
|
<DropdownMenuTrigger asChild>
|
||||||
isError={Boolean(error)}
|
<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">
|
||||||
className="w-40"
|
{selectedEventTypes?.length === 1
|
||||||
>
|
? eventTypes.find((eventType) => eventType.value === selectedEventTypes[0])
|
||||||
<Select
|
?.label
|
||||||
{...(field.value ? { value: field.value } : { placeholder: "Select" })}
|
: selectedEventTypes?.length === 0
|
||||||
{...field}
|
? "Select event types"
|
||||||
onValueChange={(e) => onChange(e)}
|
: `${selectedEventTypes?.length} events selected`}
|
||||||
className="w-full border border-mineshaft-500 bg-mineshaft-700 text-mineshaft-100"
|
<FontAwesomeIcon icon={faChevronDown} className="ml-2 text-xs" />
|
||||||
>
|
</div>
|
||||||
{eventTypes.map(({ label, value }) => (
|
</DropdownMenuTrigger>
|
||||||
<SelectItem value={String(value || "")} key={label}>
|
<DropdownMenuContent align="start" className="z-[100] max-h-80 overflow-hidden">
|
||||||
{label}
|
<div className="max-h-80 overflow-y-auto">
|
||||||
</SelectItem>
|
{eventTypes && eventTypes.length > 0 ? (
|
||||||
))}
|
eventTypes.map((eventType) => {
|
||||||
</Select>
|
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>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{!isLoading && data && data.length > 0 && !presetActor && (
|
|
||||||
|
{!isLoading && data && data.length > 0 && !presets?.actorId && (
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="actor"
|
name="actor"
|
||||||
@ -207,8 +268,8 @@ export const LogsFilter = ({ presetActor, className, control, reset }: Props) =>
|
|||||||
leftIcon={<FontAwesomeIcon icon={faFilterCircleXmark} />}
|
leftIcon={<FontAwesomeIcon icon={faFilterCircleXmark} />}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
reset({
|
reset({
|
||||||
eventType: undefined,
|
eventType: presets?.eventType || [],
|
||||||
actor: presetActor,
|
actor: presets?.actorId,
|
||||||
userAgentType: undefined,
|
userAgentType: undefined,
|
||||||
startDate: undefined,
|
startDate: undefined,
|
||||||
endDate: undefined
|
endDate: undefined
|
||||||
|
@ -5,24 +5,38 @@ import { yupResolver } from "@hookform/resolvers/yup";
|
|||||||
|
|
||||||
import { UpgradePlanModal } from "@app/components/v2";
|
import { UpgradePlanModal } from "@app/components/v2";
|
||||||
import { useSubscription } from "@app/context";
|
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 { usePopUp } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
import { LogsFilter } from "./LogsFilter";
|
import { LogsFilter } from "./LogsFilter";
|
||||||
import { LogsTable } from "./LogsTable";
|
import { LogsTable, TAuditLogTableHeader } from "./LogsTable";
|
||||||
import { AuditLogFilterFormData, auditLogFilterFormSchema } from "./types";
|
import { AuditLogFilterFormData, auditLogFilterFormSchema } from "./types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
presetActor?: string;
|
presets?: {
|
||||||
|
actorId?: string;
|
||||||
|
eventType?: EventType[];
|
||||||
|
actorType?: ActorType;
|
||||||
|
startDate?: Date;
|
||||||
|
endDate?: Date;
|
||||||
|
eventMetadata?: Record<string, string>;
|
||||||
|
};
|
||||||
|
|
||||||
showFilters?: boolean;
|
showFilters?: boolean;
|
||||||
filterClassName?: string;
|
filterClassName?: string;
|
||||||
isOrgAuditLogs?: boolean;
|
isOrgAuditLogs?: boolean;
|
||||||
|
showActorColumn?: boolean;
|
||||||
|
remappedHeaders?: Partial<Record<TAuditLogTableHeader, string>>;
|
||||||
|
refetchInterval?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const LogsSection = ({
|
export const LogsSection = ({
|
||||||
presetActor,
|
presets,
|
||||||
filterClassName,
|
filterClassName,
|
||||||
|
remappedHeaders,
|
||||||
isOrgAuditLogs,
|
isOrgAuditLogs,
|
||||||
|
showActorColumn,
|
||||||
|
refetchInterval,
|
||||||
showFilters
|
showFilters
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { subscription } = useSubscription();
|
const { subscription } = useSubscription();
|
||||||
@ -33,11 +47,12 @@ export const LogsSection = ({
|
|||||||
const { control, reset, watch } = useForm<AuditLogFilterFormData>({
|
const { control, reset, watch } = useForm<AuditLogFilterFormData>({
|
||||||
resolver: yupResolver(auditLogFilterFormSchema),
|
resolver: yupResolver(auditLogFilterFormSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
actor: presetActor,
|
actor: presets?.actorId,
|
||||||
|
eventType: presets?.eventType || [],
|
||||||
page: 1,
|
page: 1,
|
||||||
perPage: 10,
|
perPage: 10,
|
||||||
startDate: new Date(new Date().setDate(new Date().getDate() - 1)), // day before today
|
startDate: presets?.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
|
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]);
|
}, [subscription]);
|
||||||
|
|
||||||
const eventType = watch("eventType") as EventType | undefined;
|
const eventType = watch("eventType") as EventType[] | undefined;
|
||||||
const userAgentType = watch("userAgentType") as UserAgentType | undefined;
|
const userAgentType = watch("userAgentType") as UserAgentType | undefined;
|
||||||
const actor = watch("actor");
|
const actor = watch("actor");
|
||||||
|
|
||||||
@ -59,19 +74,27 @@ export const LogsSection = ({
|
|||||||
{showFilters && (
|
{showFilters && (
|
||||||
<LogsFilter
|
<LogsFilter
|
||||||
className={filterClassName}
|
className={filterClassName}
|
||||||
presetActor={presetActor}
|
presets={presets}
|
||||||
control={control}
|
control={control}
|
||||||
|
watch={watch}
|
||||||
reset={reset}
|
reset={reset}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<LogsTable
|
<LogsTable
|
||||||
|
refetchInterval={refetchInterval}
|
||||||
|
remappedHeaders={remappedHeaders}
|
||||||
isOrgAuditLogs={isOrgAuditLogs}
|
isOrgAuditLogs={isOrgAuditLogs}
|
||||||
eventType={eventType}
|
showActorColumn={!!showActorColumn && !isOrgAuditLogs}
|
||||||
userAgentType={userAgentType}
|
filter={{
|
||||||
showActorColumn={!presetActor}
|
eventMetadata: presets?.eventMetadata,
|
||||||
actor={actor}
|
actorType: presets?.actorType,
|
||||||
startDate={startDate}
|
limit: 15,
|
||||||
endDate={endDate}
|
eventType,
|
||||||
|
userAgentType,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
actorId: actor
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<UpgradePlanModal
|
<UpgradePlanModal
|
||||||
isOpen={popUp.upgradePlan.isOpen}
|
isOpen={popUp.upgradePlan.isOpen}
|
||||||
|
@ -15,43 +15,41 @@ import {
|
|||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useWorkspace } from "@app/context";
|
import { useWorkspace } from "@app/context";
|
||||||
import { useGetAuditLogs } from "@app/hooks/api";
|
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";
|
import { LogsTableRow } from "./LogsTableRow";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
eventType?: EventType;
|
|
||||||
userAgentType?: UserAgentType;
|
|
||||||
actor?: string;
|
|
||||||
startDate?: Date;
|
|
||||||
endDate?: Date;
|
|
||||||
isOrgAuditLogs?: boolean;
|
isOrgAuditLogs?: boolean;
|
||||||
showActorColumn: boolean;
|
showActorColumn: boolean;
|
||||||
|
filter?: TGetAuditLogsFilter;
|
||||||
|
remappedHeaders?: Partial<Record<TAuditLogTableHeader, string>>;
|
||||||
|
refetchInterval?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const AUDIT_LOG_LIMIT = 15;
|
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 = ({
|
export const LogsTable = ({
|
||||||
eventType,
|
|
||||||
userAgentType,
|
|
||||||
showActorColumn,
|
showActorColumn,
|
||||||
actor,
|
isOrgAuditLogs,
|
||||||
startDate,
|
filter,
|
||||||
endDate,
|
remappedHeaders,
|
||||||
isOrgAuditLogs
|
refetchInterval
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { currentWorkspace } = useWorkspace();
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
|
||||||
const { data, isLoading, isFetchingNextPage, hasNextPage, fetchNextPage } = useGetAuditLogs(
|
const { data, isLoading, isFetchingNextPage, hasNextPage, fetchNextPage } = useGetAuditLogs(
|
||||||
{
|
{
|
||||||
eventType,
|
...filter,
|
||||||
userAgentType,
|
|
||||||
actor,
|
|
||||||
startDate,
|
|
||||||
endDate,
|
|
||||||
limit: AUDIT_LOG_LIMIT
|
limit: AUDIT_LOG_LIMIT
|
||||||
},
|
},
|
||||||
!isOrgAuditLogs ? currentWorkspace?.id ?? "" : null
|
!isOrgAuditLogs ? currentWorkspace?.id ?? "" : null,
|
||||||
|
{
|
||||||
|
refetchInterval
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const isEmpty = !isLoading && !data?.pages?.[0].length;
|
const isEmpty = !isLoading && !data?.pages?.[0].length;
|
||||||
@ -62,18 +60,24 @@ export const LogsTable = ({
|
|||||||
<Table>
|
<Table>
|
||||||
<THead>
|
<THead>
|
||||||
<Tr>
|
<Tr>
|
||||||
<Th>Timestamp</Th>
|
{TABLE_HEADERS.map((header, idx) => {
|
||||||
<Th>Event</Th>
|
if (
|
||||||
{isOrgAuditLogs && <Th>Project</Th>}
|
(header === "Project" && !isOrgAuditLogs) ||
|
||||||
{showActorColumn && <Th>Actor</Th>}
|
(header === "Actor" && !showActorColumn)
|
||||||
<Th>Source</Th>
|
) {
|
||||||
<Th>Metadata</Th>
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Th key={`table-header-${idx + 1}`}>{remappedHeaders?.[header] || header}</Th>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</Tr>
|
</Tr>
|
||||||
</THead>
|
</THead>
|
||||||
<TBody>
|
<TBody>
|
||||||
{!isLoading &&
|
{!isLoading &&
|
||||||
data?.pages?.map((group, i) => (
|
data?.pages?.map((group, i) => (
|
||||||
<Fragment key={`auditlog-item-${i + 1}`}>
|
<Fragment key={`audit-log-fragment-${i + 1}`}>
|
||||||
{group.map((auditLog) => (
|
{group.map((auditLog) => (
|
||||||
<LogsTableRow
|
<LogsTableRow
|
||||||
showActorColumn={showActorColumn}
|
showActorColumn={showActorColumn}
|
||||||
|
@ -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 { eventToNameMap, userAgentTTypeoNameMap } from "@app/hooks/api/auditLogs/constants";
|
||||||
import { ActorType, EventType } from "@app/hooks/api/auditLogs/enums";
|
import { ActorType, EventType } from "@app/hooks/api/auditLogs/enums";
|
||||||
import { Actor, AuditLog, Event } from "@app/hooks/api/auditLogs/types";
|
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>
|
<p>{`Secret Request Channels: ${event.metadata.secretRequestChannels}`}</p>
|
||||||
</Td>
|
</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:
|
default:
|
||||||
return <Td />;
|
return <Td />;
|
||||||
}
|
}
|
||||||
@ -484,16 +499,41 @@ export const LogsTableRow = ({ auditLog, isOrgAuditLogs, showActorColumn }: Prop
|
|||||||
return formattedDate;
|
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 (
|
return (
|
||||||
<Tr className={`log-${auditLog.id} h-10 border-x-0 border-b border-t-0`}>
|
<Tr className={`log-${auditLog.id} h-10 border-x-0 border-b border-t-0`}>
|
||||||
<Td>{formatDate(auditLog.createdAt)}</Td>
|
<Td>{formatDate(auditLog.createdAt)}</Td>
|
||||||
<Td>{`${eventToNameMap[auditLog.event.type]}`}</Td>
|
<Td>{`${eventToNameMap[auditLog.event.type]}`}</Td>
|
||||||
{isOrgAuditLogs && <Td>{auditLog.project.name}</Td>}
|
{isOrgAuditLogs && <Td>{auditLog.project.name}</Td>}
|
||||||
{showActorColumn && renderActor(auditLog.actor)}
|
{showActorColumn && renderActor(auditLog.actor)}
|
||||||
<Td>
|
{renderSource()}
|
||||||
<p>{userAgentTTypeoNameMap[auditLog.userAgentType]}</p>
|
|
||||||
<p>{auditLog.ipAddress}</p>
|
|
||||||
</Td>
|
|
||||||
{renderMetadata(auditLog.event)}
|
{renderMetadata(auditLog.event)}
|
||||||
</Tr>
|
</Tr>
|
||||||
);
|
);
|
||||||
|
@ -4,7 +4,8 @@ import { EventType, UserAgentType } from "@app/hooks/api/auditLogs/enums";
|
|||||||
|
|
||||||
export const auditLogFilterFormSchema = yup
|
export const auditLogFilterFormSchema = yup
|
||||||
.object({
|
.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(),
|
actor: yup.string(),
|
||||||
userAgentType: yup.string().oneOf(Object.values(UserAgentType), "Invalid user agent type"),
|
userAgentType: yup.string().oneOf(Object.values(UserAgentType), "Invalid user agent type"),
|
||||||
startDate: yup.date(),
|
startDate: yup.date(),
|
||||||
|
Reference in New Issue
Block a user