Merge pull request #1186 from akhilmhdh/feat/audit-log-export

feat(audit-log): improvement in loading time
This commit is contained in:
BlackMagiq
2023-11-23 12:21:56 +07:00
committed by GitHub
16 changed files with 1178 additions and 944 deletions
+181 -62
View File
@@ -306,12 +306,77 @@
}, },
"/api/v1/workspace/{workspaceId}/audit-logs": { "/api/v1/workspace/{workspaceId}/audit-logs": {
"get": { "get": {
"description": "", "summary": "Return audit logs",
"description": "Return audit logs",
"parameters": [ "parameters": [
{ {
"name": "workspaceId", "name": "workspaceId",
"in": "path", "in": "path",
"required": true, "required": true,
"schema": {
"type": "string"
},
"description": "ID of the workspace where to get folders from"
},
{
"name": "offset",
"description": "Number of logs to skip before starting to return logs for pagination",
"required": false,
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "limit",
"description": "Maximum number of logs to return for pagination",
"required": false,
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "startDate",
"description": "Filter logs from this date in ISO-8601 format",
"required": false,
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "endDate",
"description": "Filter logs till this date in ISO-8601 format",
"required": false,
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "eventType",
"description": "Filter by type of event such as get-secrets, get-secret, create-secret, update-secret, delete-secret, etc.",
"required": false,
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "userAgentType",
"description": "Filter by type of user agent such as web, cli, k8-operator, or other",
"required": false,
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "actor",
"description": "Filter by actor such as user or service",
"required": false,
"in": "query",
"schema": { "schema": {
"type": "string" "type": "string"
} }
@@ -319,9 +384,30 @@
], ],
"responses": { "responses": {
"200": { "200": {
"description": "OK" "description": "OK",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"auditLogs": {
"type": "array",
"items": {
"$ref": "#/components/schemas/AuditLog"
},
"description": "List of audit log"
}
}
}
}
}
} }
} },
"security": [
{
"apiKeyAuth": []
}
]
} }
}, },
"/api/v1/workspace/{workspaceId}/audit-logs/filters/actors": { "/api/v1/workspace/{workspaceId}/audit-logs/filters/actors": {
@@ -1132,6 +1218,43 @@
} }
} }
}, },
"/api/v1/admin/config": {
"get": {
"description": "",
"responses": {
"200": {
"description": "OK"
}
}
},
"patch": {
"description": "",
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/api/v1/admin/signup": {
"post": {
"description": "",
"parameters": [
{
"name": "user-agent",
"in": "header",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/api/v1/bot/{workspaceId}": { "/api/v1/bot/{workspaceId}": {
"get": { "get": {
"description": "", "description": "",
@@ -4560,65 +4683,6 @@
} }
} }
} }
},
"get": {
"summary": "Get all accessible environments of a workspace",
"description": "Fetch all environments that the user has access to in a specified workspace",
"parameters": [
{
"name": "workspaceId",
"in": "path",
"required": true,
"schema": {
"type": "string"
},
"description": "ID of the workspace"
}
],
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"accessibleEnvironments": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string",
"example": "Development"
},
"slug": {
"type": "string",
"example": "development"
},
"isWriteDenied": {
"type": "boolean",
"example": false
},
"isReadDenied": {
"type": "boolean",
"example": false
}
}
}
}
},
"description": "List of environments the user has access to in the specified workspace"
}
}
}
}
},
"security": [
{
"apiKeyAuth": []
}
]
} }
}, },
"/api/v2/workspace/{workspaceId}/tags": { "/api/v2/workspace/{workspaceId}/tags": {
@@ -6864,6 +6928,61 @@
"example": "2023-01-13T14:16:12.210Z" "example": "2023-01-13T14:16:12.210Z"
} }
} }
},
"AuditLog": {
"type": "object",
"properties": {
"actor": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": ""
},
"metadata": {
"type": "object",
"properties": {}
}
}
},
"organization": {
"type": "string",
"example": ""
},
"workspace": {
"type": "string",
"example": ""
},
"ipAddress": {
"type": "string",
"example": ""
},
"event": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": ""
},
"metadata": {
"type": "object",
"properties": {}
}
}
},
"userAgent": {
"type": "string",
"example": ""
},
"userAgentType": {
"type": "string",
"example": ""
},
"expiresAt": {
"type": "string",
"example": ""
}
}
} }
}, },
"securitySchemes": { "securitySchemes": {
@@ -578,6 +578,82 @@ export const rollbackWorkspaceSecretSnapshot = async (req: Request, res: Respons
* @param res * @param res
*/ */
export const getWorkspaceAuditLogs = async (req: Request, res: Response) => { export const getWorkspaceAuditLogs = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Return audit logs'
#swagger.description = 'Return audit logs'
#swagger.security = [{
"apiKeyAuth": []
}]
#swagger.parameters['workspaceId'] = {
"description": "ID of the workspace where to get folders from",
"required": true,
"type": "string",
"in": "path"
}
#swagger.parameters['offset'] = {
"description": "Number of logs to skip before starting to return logs for pagination",
"required": false,
"type": "string"
}
#swagger.parameters['limit'] = {
"description": "Maximum number of logs to return for pagination",
"required": false,
"type": "string"
}
#swagger.parameters['startDate'] = {
"description": "Filter logs from this date in ISO-8601 format",
"required": false,
"type": "string"
}
#swagger.parameters['endDate'] = {
"description": "Filter logs till this date in ISO-8601 format",
"required": false,
"type": "string"
}
#swagger.parameters['eventType'] = {
"description": "Filter by type of event such as get-secrets, get-secret, create-secret, update-secret, delete-secret, etc.",
"required": false,
"type": "string",
}
#swagger.parameters['userAgentType'] = {
"description": "Filter by type of user agent such as web, cli, k8-operator, or other",
"required": false,
"type": "string",
}
#swagger.parameters['actor'] = {
"description": "Filter by actor such as user or service",
"required": false,
"type": "string"
}
#swagger.responses[200] = {
content: {
"application/json": {
schema: {
"type": "object",
"properties": {
"auditLogs": {
"type": "array",
"items": {
$ref: "#/components/schemas/AuditLog",
},
"description": "List of audit log"
},
}
}
}
}
}
*/
const { const {
query: { limit, offset, endDate, eventType, startDate, userAgentType, actor }, query: { limit, offset, endDate, eventType, startDate, userAgentType, actor },
params: { workspaceId } params: { workspaceId }
@@ -592,7 +668,7 @@ export const getWorkspaceAuditLogs = async (req: Request, res: Response) => {
ProjectPermissionActions.Read, ProjectPermissionActions.Read,
ProjectPermissionSub.AuditLogs ProjectPermissionSub.AuditLogs
); );
const query = { const query = {
workspace: new Types.ObjectId(workspaceId), workspace: new Types.ObjectId(workspaceId),
...(eventType ...(eventType
@@ -626,14 +702,9 @@ export const getWorkspaceAuditLogs = async (req: Request, res: Response) => {
} }
: {}) : {})
}; };
const auditLogs = await AuditLog.find(query).sort({ createdAt: -1 }).skip(offset).limit(limit); const auditLogs = await AuditLog.find(query).sort({ createdAt: -1 }).skip(offset).limit(limit);
const totalCount = await AuditLog.countDocuments(query);
return res.status(200).send({ return res.status(200).send({
auditLogs, auditLogs
totalCount
}); });
}; };
@@ -685,7 +756,7 @@ export const getWorkspaceAuditLogActorFilterOpts = async (req: Request, res: Res
name: serviceTokenData.name name: serviceTokenData.name
} }
})); }));
const serviceV3Actors: ServiceActorV3[] = ( const serviceV3Actors: ServiceActorV3[] = (
await ServiceTokenDataV3.find({ await ServiceTokenDataV3.find({
workspace: new Types.ObjectId(workspaceId) workspace: new Types.ObjectId(workspaceId)
@@ -697,12 +768,8 @@ export const getWorkspaceAuditLogActorFilterOpts = async (req: Request, res: Res
name: serviceTokenData.name name: serviceTokenData.name
} }
})); }));
const actors = [ const actors = [...userActors, ...serviceActors, ...serviceV3Actors];
...userActors,
...serviceActors,
...serviceV3Actors
];
return res.status(200).send({ return res.status(200).send({
actors actors
+60 -66
View File
@@ -1,76 +1,70 @@
import { Schema, Types, model } from "mongoose"; import { Schema, Types, model } from "mongoose";
import { import { ActorType, EventType, UserAgentType } from "./enums";
ActorType, import { Actor, Event } from "./types";
EventType,
UserAgentType
} from "./enums";
import {
Actor,
Event
} from "./types";
export interface IAuditLog { export interface IAuditLog {
actor: Actor; actor: Actor;
organization: Types.ObjectId; organization: Types.ObjectId;
workspace: Types.ObjectId; workspace: Types.ObjectId;
ipAddress: string; ipAddress: string;
event: Event; event: Event;
userAgent: string; userAgent: string;
userAgentType: UserAgentType; userAgentType: UserAgentType;
expiresAt: Date; expiresAt: Date;
} }
const auditLogSchema = new Schema<IAuditLog>( const auditLogSchema = new Schema<IAuditLog>(
{ {
actor: { actor: {
type: { type: {
type: String, type: String,
enum: ActorType, enum: ActorType,
required: true required: true
}, },
metadata: { metadata: {
type: Schema.Types.Mixed type: Schema.Types.Mixed
} }
},
organization: {
type: Schema.Types.ObjectId,
required: false
},
workspace: {
type: Schema.Types.ObjectId,
required: false
},
ipAddress: {
type: String,
required: true
},
event: {
type: {
type: String,
enum: EventType,
required: true
},
metadata: {
type: Schema.Types.Mixed
}
},
userAgent: {
type: String,
required: true
},
userAgentType: {
type: String,
enum: UserAgentType,
required: true
},
expiresAt: {
type: Date,
expires: 0
}
}, },
{ organization: {
timestamps: true type: Schema.Types.ObjectId,
required: false
},
workspace: {
type: Schema.Types.ObjectId,
required: false,
index: true
},
ipAddress: {
type: String,
required: true
},
event: {
type: {
type: String,
enum: EventType,
required: true
},
metadata: {
type: Schema.Types.Mixed
}
},
userAgent: {
type: String,
required: true
},
userAgentType: {
type: String,
enum: UserAgentType,
required: true
},
expiresAt: {
type: Date,
expires: 0
} }
},
{
timestamps: true
}
); );
export const AuditLog = model<IAuditLog>("AuditLog", auditLogSchema); export const AuditLog = model<IAuditLog>("AuditLog", auditLogSchema);
+7 -7
View File
@@ -5,12 +5,12 @@ export let logger: Logger;
// https://github.com/pinojs/pino/blob/master/lib/levels.js#L13-L20 // https://github.com/pinojs/pino/blob/master/lib/levels.js#L13-L20
const logLevelToSeverityLookup: Record<string, string> = { const logLevelToSeverityLookup: Record<string, string> = {
'10': 'TRACE', "10": "TRACE",
'20': 'DEBUG', "20": "DEBUG",
'30': 'INFO', "30": "INFO",
'40': 'WARNING', "40": "WARNING",
'50': 'ERROR', "50": "ERROR",
'60': 'CRITICAL' "60": "CRITICAL"
} }
export const initLogger = async () => { export const initLogger = async () => {
@@ -51,7 +51,7 @@ export const initLogger = async () => {
logger = pino( logger = pino(
{ {
mixin(_context, level) { mixin(_context, level) {
return { 'severity': logLevelToSeverityLookup[level] || logLevelToSeverityLookup['30'] } return { "severity": logLevelToSeverityLookup[level] || logLevelToSeverityLookup["30"] }
}, },
level: process.env.PINO_LOG_LEVEL || "info", level: process.env.PINO_LOG_LEVEL || "info",
formatters: { formatters: {
+4 -4
View File
@@ -58,7 +58,7 @@ export const validateClientForWorkspace = async ({
environment, environment,
requiredPermissions requiredPermissions
}); });
return { membership, workspace}; return { membership, workspace };
case ActorType.SERVICE_V3: case ActorType.SERVICE_V3:
throw UnauthorizedRequestError({ throw UnauthorizedRequestError({
message: "Failed service token authorization for organization" message: "Failed service token authorization for organization"
@@ -121,8 +121,8 @@ export const GetWorkspaceAuditLogsV1 = z.object({
userAgentType: z.nativeEnum(UserAgentType).nullable().optional(), userAgentType: z.nativeEnum(UserAgentType).nullable().optional(),
startDate: z.string().datetime().nullable().optional(), startDate: z.string().datetime().nullable().optional(),
endDate: z.string().datetime().nullable().optional(), endDate: z.string().datetime().nullable().optional(),
offset: z.coerce.number(), offset: z.coerce.number().default(0),
limit: z.coerce.number(), limit: z.coerce.number().default(20),
actor: z.string().nullish().optional() actor: z.string().nullish().optional()
}) })
}); });
@@ -309,4 +309,4 @@ export const GetWorkspaceServiceTokenDataV3 = z.object({
params: z.object({ params: z.object({
workspaceId: z.string().trim() workspaceId: z.string().trim()
}) })
}); });
+57 -41
View File
@@ -12,32 +12,32 @@ const generateOpenAPISpec = async () => {
const doc = { const doc = {
info: { info: {
title: "Infisical API", title: "Infisical API",
description: "List of all available APIs that can be consumed", description: "List of all available APIs that can be consumed"
}, },
host: ["https://infisical.com"], host: ["https://infisical.com"],
servers: [ servers: [
{ {
url: "https://app.infisical.com", url: "https://app.infisical.com",
description: "Production server", description: "Production server"
}, },
{ {
url: "http://localhost:8080", url: "http://localhost:8080",
description: "Local server", description: "Local server"
}, }
], ],
securityDefinitions: { securityDefinitions: {
bearerAuth: { bearerAuth: {
type: "http", type: "http",
scheme: "bearer", scheme: "bearer",
bearerFormat: "JWT", bearerFormat: "JWT",
description: "A service token in Infisical", description: "A service token in Infisical"
}, },
apiKeyAuth: { apiKeyAuth: {
type: "apiKey", type: "apiKey",
in: "header", in: "header",
name: "X-API-Key", name: "X-API-Key",
description: "An API Key in Infisical", description: "An API Key in Infisical"
}, }
}, },
definitions: { definitions: {
CurrentUser: { CurrentUser: {
@@ -50,7 +50,7 @@ const generateOpenAPISpec = async () => {
iv: "iv_of_enc_nacl_private_key", iv: "iv_of_enc_nacl_private_key",
tag: "tag_of_enc_nacl_private_key", tag: "tag_of_enc_nacl_private_key",
updatedAt: "2023-01-13T14:16:12.210Z", updatedAt: "2023-01-13T14:16:12.210Z",
createdAt: "2023-01-13T14:16:12.210Z", createdAt: "2023-01-13T14:16:12.210Z"
}, },
Membership: { Membership: {
user: { user: {
@@ -60,10 +60,10 @@ const generateOpenAPISpec = async () => {
lastName: "Doe", lastName: "Doe",
publicKey: "johns_nacl_public_key", publicKey: "johns_nacl_public_key",
updatedAt: "2023-01-13T14:16:12.210Z", updatedAt: "2023-01-13T14:16:12.210Z",
createdAt: "2023-01-13T14:16:12.210Z", createdAt: "2023-01-13T14:16:12.210Z"
}, },
workspace: "", workspace: "",
role: "admin", role: "admin"
}, },
MembershipOrg: { MembershipOrg: {
user: { user: {
@@ -73,33 +73,35 @@ const generateOpenAPISpec = async () => {
lastName: "Doe", lastName: "Doe",
publicKey: "johns_nacl_public_key", publicKey: "johns_nacl_public_key",
updatedAt: "2023-01-13T14:16:12.210Z", updatedAt: "2023-01-13T14:16:12.210Z",
createdAt: "2023-01-13T14:16:12.210Z", createdAt: "2023-01-13T14:16:12.210Z"
}, },
organization: "", organization: "",
role: "owner", role: "owner",
status: "accepted", status: "accepted"
}, },
Organization: { Organization: {
_id: "", _id: "",
name: "Acme Corp.", name: "Acme Corp.",
customerId: "", customerId: ""
}, },
Project: { Project: {
name: "My Project", name: "My Project",
organization: "", organization: "",
environments: [{ environments: [
name: "development", {
slug: "dev", name: "development",
}], slug: "dev"
}
]
}, },
ProjectKey: { ProjectKey: {
encryptedkey: "", encryptedkey: "",
nonce: "", nonce: "",
sender: { sender: {
publicKey: "senders_nacl_public_key", publicKey: "senders_nacl_public_key"
}, },
receiver: "", receiver: "",
workspace: "", workspace: ""
}, },
CreateSecret: { CreateSecret: {
type: "shared", type: "shared",
@@ -111,7 +113,7 @@ const generateOpenAPISpec = async () => {
secretValueTag: "", secretValueTag: "",
secretCommentCiphertext: "", secretCommentCiphertext: "",
secretCommentIV: "", secretCommentIV: "",
secretCommentTag: "", secretCommentTag: ""
}, },
UpdateSecret: { UpdateSecret: {
id: "", id: "",
@@ -123,12 +125,12 @@ const generateOpenAPISpec = async () => {
secretValueTag: "", secretValueTag: "",
secretCommentCiphertext: "", secretCommentCiphertext: "",
secretCommentIV: "", secretCommentIV: "",
secretCommentTag: "", secretCommentTag: ""
}, },
Secret: { Secret: {
_id: "", _id: "",
version: 1, version: 1,
workspace : "", workspace: "",
type: "shared", type: "shared",
user: null, user: null,
secretKeyCiphertext: "", secretKeyCiphertext: "",
@@ -141,7 +143,7 @@ const generateOpenAPISpec = async () => {
secretCommentIV: "", secretCommentIV: "",
secretCommentTag: "", secretCommentTag: "",
updatedAt: "2023-01-13T14:16:12.210Z", updatedAt: "2023-01-13T14:16:12.210Z",
createdAt: "2023-01-13T14:16:12.210Z", createdAt: "2023-01-13T14:16:12.210Z"
}, },
RawSecret: { RawSecret: {
_id: "abc123", _id: "abc123",
@@ -167,12 +169,10 @@ const generateOpenAPISpec = async () => {
_id: "", _id: "",
email: "[email protected]", email: "[email protected]",
firstName: "John", firstName: "John",
lastName: "Doe", lastName: "Doe"
}, },
workspace: "", workspace: "",
actionNames: [ actionNames: ["addSecrets"],
"addSecrets",
],
actions: [ actions: [
{ {
name: "addSecrets", name: "addSecrets",
@@ -181,24 +181,24 @@ const generateOpenAPISpec = async () => {
payload: [ payload: [
{ {
oldSecretVersion: "", oldSecretVersion: "",
newSecretVersion: "", newSecretVersion: ""
}, }
], ]
}, }
], ],
channel: "cli", channel: "cli",
ipAddress: "192.168.0.1", ipAddress: "192.168.0.1",
updatedAt: "2023-01-13T14:16:12.210Z", updatedAt: "2023-01-13T14:16:12.210Z",
createdAt: "2023-01-13T14:16:12.210Z", createdAt: "2023-01-13T14:16:12.210Z"
}, },
SecretSnapshot: { SecretSnapshot: {
workspace: "", workspace: "",
version: 1, version: 1,
secretVersions: [ secretVersions: [
{ {
_id: "", _id: ""
}, }
], ]
}, },
SecretVersion: { SecretVersion: {
_id: "", _id: "",
@@ -214,7 +214,7 @@ const generateOpenAPISpec = async () => {
secretKeyTag: "", secretKeyTag: "",
secretValueCiphertext: "", secretValueCiphertext: "",
secretValueIV: "", secretValueIV: "",
secretValueTag: "", secretValueTag: ""
}, },
ServiceTokenData: { ServiceTokenData: {
_id: "", _id: "",
@@ -224,16 +224,32 @@ const generateOpenAPISpec = async () => {
user: { user: {
_id: "", _id: "",
firstName: "", firstName: "",
lastName: "", lastName: ""
}, },
expiresAt: "2023-01-13T14:16:12.210Z", expiresAt: "2023-01-13T14:16:12.210Z",
encryptedKey: "", encryptedKey: "",
iv: "", iv: "",
tag: "", tag: "",
updatedAt: "2023-01-13T14:16:12.210Z", updatedAt: "2023-01-13T14:16:12.210Z",
createdAt: "2023-01-13T14:16:12.210Z", createdAt: "2023-01-13T14:16:12.210Z"
}, },
}, AuditLog: {
actor: {
type: "",
metadata: {}
},
organization: "",
workspace: "",
ipAddress: "",
event: {
type: "",
metadata: {}
},
userAgent: "",
userAgentType: "",
expiresAt: ""
}
}
}; };
const outputJSONFile = "../spec.json"; const outputJSONFile = "../spec.json";
@@ -243,6 +259,6 @@ const generateOpenAPISpec = async () => {
const spec = await swaggerAutogen(outputJSONFile, endpointsFiles, doc); const spec = await swaggerAutogen(outputJSONFile, endpointsFiles, doc);
await fs.writeFile(outputYAMLFile, yaml.dump(spec.data)); await fs.writeFile(outputYAMLFile, yaml.dump(spec.data));
} };
generateOpenAPISpec(); generateOpenAPISpec();
@@ -0,0 +1,4 @@
---
title: "Export"
openapi: "GET /api/v1/workspace/{workspaceId}/audit-logs"
---
@@ -1,4 +1,4 @@
--- ---
title: "Create" title: "Create"
openapi: "POST /api/v2/workspace/{workspaceId}/environments" openapi: "POST /api/v1/workspace/{workspaceId}/environments"
--- ---
+4
View File
@@ -380,6 +380,10 @@
{ {
"group": "Service Tokens", "group": "Service Tokens",
"pages": ["api-reference/endpoints/service-tokens/get"] "pages": ["api-reference/endpoints/service-tokens/get"]
},
{
"group": "Audit Logs",
"pages": ["api-reference/endpoints/audit-logs/export-audit-log"]
} }
] ]
}, },
+120 -43
View File
@@ -192,16 +192,74 @@ paths:
description: Version of secret snapshot to roll back to description: Version of secret snapshot to roll back to
/api/v1/workspace/{workspaceId}/audit-logs: /api/v1/workspace/{workspaceId}/audit-logs:
get: get:
description: '' summary: Return audit logs
description: Return audit logs
parameters: parameters:
- name: workspaceId - name: workspaceId
in: path in: path
required: true required: true
schema: schema:
type: string type: string
description: ID of the workspace where to get folders from
- name: offset
description: Number of logs to skip before starting to return logs for pagination
required: false
in: query
schema:
type: string
- name: limit
description: Maximum number of logs to return for pagination
required: false
in: query
schema:
type: string
- name: startDate
description: Filter logs from this date in ISO-8601 format
required: false
in: query
schema:
type: string
- name: endDate
description: Filter logs till this date in ISO-8601 format
required: false
in: query
schema:
type: string
- name: eventType
description: >-
Filter by type of event such as get-secrets, get-secret,
create-secret, update-secret, delete-secret, etc.
required: false
in: query
schema:
type: string
- name: userAgentType
description: Filter by type of user agent such as web, cli, k8-operator, or other
required: false
in: query
schema:
type: string
- name: actor
description: Filter by actor such as user or service
required: false
in: query
schema:
type: string
responses: responses:
'200': '200':
description: OK description: OK
content:
application/json:
schema:
type: object
properties:
auditLogs:
type: array
items:
$ref: '#/components/schemas/AuditLog'
description: List of audit log
security:
- apiKeyAuth: []
/api/v1/workspace/{workspaceId}/audit-logs/filters/actors: /api/v1/workspace/{workspaceId}/audit-logs/filters/actors:
get: get:
description: '' description: ''
@@ -691,6 +749,28 @@ paths:
responses: responses:
'200': '200':
description: OK description: OK
/api/v1/admin/config:
get:
description: ''
responses:
'200':
description: OK
patch:
description: ''
responses:
'200':
description: OK
/api/v1/admin/signup:
post:
description: ''
parameters:
- name: user-agent
in: header
schema:
type: string
responses:
'200':
description: OK
/api/v1/bot/{workspaceId}: /api/v1/bot/{workspaceId}:
get: get:
description: '' description: ''
@@ -2834,48 +2914,6 @@ paths:
example: dev example: dev
required: required:
- environmentSlug - environmentSlug
get:
summary: Get all accessible environments of a workspace
description: >-
Fetch all environments that the user has access to in a specified
workspace
parameters:
- name: workspaceId
in: path
required: true
schema:
type: string
description: ID of the workspace
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
accessibleEnvironments:
type: array
items:
type: object
properties:
name:
type: string
example: Development
slug:
type: string
example: development
isWriteDenied:
type: boolean
example: false
isReadDenied:
type: boolean
example: false
description: >-
List of environments the user has access to in the specified
workspace
security:
- apiKeyAuth: []
/api/v2/workspace/{workspaceId}/tags: /api/v2/workspace/{workspaceId}/tags:
get: get:
description: '' description: ''
@@ -4342,6 +4380,45 @@ components:
createdAt: createdAt:
type: string type: string
example: '2023-01-13T14:16:12.210Z' example: '2023-01-13T14:16:12.210Z'
AuditLog:
type: object
properties:
actor:
type: object
properties:
type:
type: string
example: ''
metadata:
type: object
properties: {}
organization:
type: string
example: ''
workspace:
type: string
example: ''
ipAddress:
type: string
example: ''
event:
type: object
properties:
type:
type: string
example: ''
metadata:
type: object
properties: {}
userAgent:
type: string
example: ''
userAgentType:
type: string
example: ''
expiresAt:
type: string
example: ''
securitySchemes: securitySchemes:
bearerAuth: bearerAuth:
type: http type: http
+36 -49
View File
@@ -1,59 +1,46 @@
import { useQuery } from "@tanstack/react-query"; import { useInfiniteQuery, useQuery } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request"; import { apiRequest } from "@app/config/request";
import { import { Actor, AuditLog, AuditLogFilters } from "./types";
Actor,
AuditLog,
AuditLogFilters
} from "./types";
export const workspaceKeys = { export const workspaceKeys = {
getAuditLogs: (workspaceId: string, filters: AuditLogFilters) => [{ workspaceId, filters }, "audit-logs"] as const, getAuditLogs: (workspaceId: string, filters: AuditLogFilters) =>
getAuditLogActorFilterOpts: (workspaceId: string) => [{ workspaceId }, "audit-log-actor-filters"] as const [{ workspaceId, filters }, "audit-logs"] as const,
} getAuditLogActorFilterOpts: (workspaceId: string) =>
[{ workspaceId }, "audit-log-actor-filters"] as const
};
export const useGetAuditLogs = (workspaceId: string, filters: AuditLogFilters) => { export const useGetAuditLogs = (workspaceId: string, filters: AuditLogFilters) => {
return useQuery({ return useInfiniteQuery({
queryKey: workspaceKeys.getAuditLogs(workspaceId, filters), queryKey: workspaceKeys.getAuditLogs(workspaceId, filters),
queryFn: async () => { queryFn: async ({ pageParam }) => {
const { data } = await apiRequest.get<{ auditLogs: AuditLog[] }>(
const params = new URLSearchParams(); `/api/v1/workspace/${workspaceId}/audit-logs`,
if (filters.eventType) { {
params.append("eventType", filters.eventType); params: {
} ...filters,
offset: pageParam,
if (filters.userAgentType) { startDate: filters?.startDate?.toISOString(),
params.append("userAgentType", filters.userAgentType); endDate: filters?.endDate?.toISOString()
} }
if (filters.actor) {
params.append("actor", filters.actor);
}
if (filters.startDate) {
params.append("startDate", filters.startDate.toISOString());
}
if (filters.endDate) {
params.append("endDate", filters.endDate.toISOString());
}
params.append("offset", String(filters.offset));
params.append("limit", String(filters.limit));
const { data } = await apiRequest.get<{ auditLogs: AuditLog[], totalCount: number }>(`/api/v1/workspace/${workspaceId}/audit-logs`, { params });
return data;
} }
}); );
} return data.auditLogs;
},
getNextPageParam: (lastPage, pages) =>
lastPage.length !== 0 ? pages.length * filters.limit : undefined
});
};
export const useGetAuditLogActorFilterOpts = (workspaceId: string) => { export const useGetAuditLogActorFilterOpts = (workspaceId: string) => {
return useQuery({ return useQuery({
queryKey: workspaceKeys.getAuditLogActorFilterOpts(workspaceId), queryKey: workspaceKeys.getAuditLogActorFilterOpts(workspaceId),
queryFn: async () => { queryFn: async () => {
const { data } = await apiRequest.get<{ actors: Actor[] }>(`/api/v1/workspace/${workspaceId}/audit-logs/filters/actors`); const { data } = await apiRequest.get<{ actors: Actor[] }>(
return data.actors; `/api/v1/workspace/${workspaceId}/audit-logs/filters/actors`
} );
}); return data.actors;
} }
});
};
+294 -303
View File
@@ -1,8 +1,4 @@
import { import { ActorType, EventType, UserAgentType } from "./enums";
ActorType,
EventType,
UserAgentType
} from "./enums";
interface UserActorMetadata { interface UserActorMetadata {
userId: string; userId: string;
@@ -25,174 +21,171 @@ export interface ServiceActor {
} }
export interface ServiceActorV3 { export interface ServiceActorV3 {
type: ActorType.SERVICE_V3; type: ActorType.SERVICE_V3;
metadata: ServiceActorMetadata; metadata: ServiceActorMetadata;
} }
export type Actor = export type Actor = UserActor | ServiceActor | ServiceActorV3;
| UserActor
| ServiceActor
| ServiceActorV3;
interface GetSecretsEvent { interface GetSecretsEvent {
type: EventType.GET_SECRETS; type: EventType.GET_SECRETS;
metadata: { metadata: {
environment: string; environment: string;
secretPath: string; secretPath: string;
numberOfSecrets: number; numberOfSecrets: number;
}; };
} }
interface GetSecretEvent { interface GetSecretEvent {
type: EventType.GET_SECRET; type: EventType.GET_SECRET;
metadata: { metadata: {
environment: string; environment: string;
secretPath: string; secretPath: string;
secretId: string; secretId: string;
secretKey: string; secretKey: string;
secretVersion: number; secretVersion: number;
}; };
} }
interface CreateSecretEvent { interface CreateSecretEvent {
type: EventType.CREATE_SECRET; type: EventType.CREATE_SECRET;
metadata: { metadata: {
environment: string; environment: string;
secretPath: string; secretPath: string;
secretId: string; secretId: string;
secretKey: string; secretKey: string;
secretVersion: number; secretVersion: number;
} };
} }
interface UpdateSecretEvent { interface UpdateSecretEvent {
type: EventType.UPDATE_SECRET; type: EventType.UPDATE_SECRET;
metadata: { metadata: {
environment: string; environment: string;
secretPath: string; secretPath: string;
secretId: string; secretId: string;
secretKey: string; secretKey: string;
secretVersion: number; secretVersion: number;
} };
} }
interface DeleteSecretEvent { interface DeleteSecretEvent {
type: EventType.DELETE_SECRET; type: EventType.DELETE_SECRET;
metadata: { metadata: {
environment: string; environment: string;
secretPath: string; secretPath: string;
secretId: string; secretId: string;
secretKey: string; secretKey: string;
secretVersion: number; secretVersion: number;
} };
} }
interface GetWorkspaceKeyEvent { interface GetWorkspaceKeyEvent {
type: EventType.GET_WORKSPACE_KEY, type: EventType.GET_WORKSPACE_KEY;
metadata: { metadata: {
keyId: string; keyId: string;
} };
} }
interface AuthorizeIntegrationEvent { interface AuthorizeIntegrationEvent {
type: EventType.AUTHORIZE_INTEGRATION; type: EventType.AUTHORIZE_INTEGRATION;
metadata: { metadata: {
integration: string; integration: string;
} };
} }
interface UnauthorizeIntegrationEvent { interface UnauthorizeIntegrationEvent {
type: EventType.UNAUTHORIZE_INTEGRATION; type: EventType.UNAUTHORIZE_INTEGRATION;
metadata: { metadata: {
integration: string; integration: string;
} };
} }
interface CreateIntegrationEvent { interface CreateIntegrationEvent {
type: EventType.CREATE_INTEGRATION; type: EventType.CREATE_INTEGRATION;
metadata: { metadata: {
integrationId: string; integrationId: string;
integration: string; integration: string;
environment: string; environment: string;
secretPath: string; secretPath: string;
url?: string; url?: string;
app?: string; app?: string;
appId?: string; appId?: string;
targetEnvironment?: string; targetEnvironment?: string;
targetEnvironmentId?: string; targetEnvironmentId?: string;
targetService?: string; targetService?: string;
targetServiceId?: string; targetServiceId?: string;
path?: string; path?: string;
region?: string; region?: string;
} };
} }
interface DeleteIntegrationEvent { interface DeleteIntegrationEvent {
type: EventType.DELETE_INTEGRATION; type: EventType.DELETE_INTEGRATION;
metadata: { metadata: {
integrationId: string; integrationId: string;
integration: string; integration: string;
environment: string; environment: string;
secretPath: string; secretPath: string;
url?: string; url?: string;
app?: string; app?: string;
appId?: string; appId?: string;
targetEnvironment?: string; targetEnvironment?: string;
targetEnvironmentId?: string; targetEnvironmentId?: string;
targetService?: string; targetService?: string;
targetServiceId?: string; targetServiceId?: string;
path?: string; path?: string;
region?: string; region?: string;
} };
} }
interface AddTrustedIPEvent { interface AddTrustedIPEvent {
type: EventType.ADD_TRUSTED_IP; type: EventType.ADD_TRUSTED_IP;
metadata: { metadata: {
trustedIpId: string; trustedIpId: string;
ipAddress: string; ipAddress: string;
prefix?: number; prefix?: number;
} };
} }
interface UpdateTrustedIPEvent { interface UpdateTrustedIPEvent {
type: EventType.UPDATE_TRUSTED_IP; type: EventType.UPDATE_TRUSTED_IP;
metadata: { metadata: {
trustedIpId: string; trustedIpId: string;
ipAddress: string; ipAddress: string;
prefix?: number; prefix?: number;
} };
} }
interface DeleteTrustedIPEvent { interface DeleteTrustedIPEvent {
type: EventType.DELETE_TRUSTED_IP; type: EventType.DELETE_TRUSTED_IP;
metadata: { metadata: {
trustedIpId: string; trustedIpId: string;
ipAddress: string; ipAddress: string;
prefix?: number; prefix?: number;
} };
} }
interface CreateServiceTokenEvent { interface CreateServiceTokenEvent {
type: EventType.CREATE_SERVICE_TOKEN; type: EventType.CREATE_SERVICE_TOKEN;
metadata: { metadata: {
name: string; name: string;
scopes: Array<{ scopes: Array<{
environment: string; environment: string;
secretPath: string; secretPath: string;
}>; }>;
} };
} }
interface DeleteServiceTokenEvent { interface DeleteServiceTokenEvent {
type: EventType.DELETE_SERVICE_TOKEN; type: EventType.DELETE_SERVICE_TOKEN;
metadata: { metadata: {
name: string; name: string;
scopes: Array<{ scopes: Array<{
environment: string; environment: string;
secretPath: string; secretPath: string;
}>; }>;
} };
} }
interface CreateServiceTokenV3Event { interface CreateServiceTokenV3Event {
@@ -226,227 +219,226 @@ interface DeleteServiceTokenV3Event {
} }
interface CreateEnvironmentEvent { interface CreateEnvironmentEvent {
type: EventType.CREATE_ENVIRONMENT; type: EventType.CREATE_ENVIRONMENT;
metadata: { metadata: {
name: string; name: string;
slug: string; slug: string;
} };
} }
interface UpdateEnvironmentEvent { interface UpdateEnvironmentEvent {
type: EventType.UPDATE_ENVIRONMENT; type: EventType.UPDATE_ENVIRONMENT;
metadata: { metadata: {
oldName: string; oldName: string;
newName: string; newName: string;
oldSlug: string; oldSlug: string;
newSlug: string; newSlug: string;
} };
} }
interface DeleteEnvironmentEvent { interface DeleteEnvironmentEvent {
type: EventType.DELETE_ENVIRONMENT; type: EventType.DELETE_ENVIRONMENT;
metadata: { metadata: {
name: string; name: string;
slug: string; slug: string;
} };
} }
interface AddWorkspaceMemberEvent { interface AddWorkspaceMemberEvent {
type: EventType.ADD_WORKSPACE_MEMBER; type: EventType.ADD_WORKSPACE_MEMBER;
metadata: { metadata: {
userId: string; userId: string;
email: string; email: string;
} };
} }
interface RemoveWorkspaceMemberEvent { interface RemoveWorkspaceMemberEvent {
type: EventType.REMOVE_WORKSPACE_MEMBER; type: EventType.REMOVE_WORKSPACE_MEMBER;
metadata: { metadata: {
userId: string; userId: string;
email: string; email: string;
} };
} }
interface CreateFolderEvent { interface CreateFolderEvent {
type: EventType.CREATE_FOLDER; type: EventType.CREATE_FOLDER;
metadata: { metadata: {
environment: string; environment: string;
folderId: string; folderId: string;
folderName: string; folderName: string;
folderPath: string; folderPath: string;
} };
} }
interface UpdateFolderEvent { interface UpdateFolderEvent {
type: EventType.UPDATE_FOLDER; type: EventType.UPDATE_FOLDER;
metadata: { metadata: {
environment: string; environment: string;
folderId: string; folderId: string;
oldFolderName: string; oldFolderName: string;
newFolderName: string; newFolderName: string;
folderPath: string; folderPath: string;
} };
} }
interface DeleteFolderEvent { interface DeleteFolderEvent {
type: EventType.DELETE_FOLDER; type: EventType.DELETE_FOLDER;
metadata: { metadata: {
environment: string; environment: string;
folderId: string; folderId: string;
folderName: string; folderName: string;
folderPath: string; folderPath: string;
} };
} }
interface CreateWebhookEvent { interface CreateWebhookEvent {
type: EventType.CREATE_WEBHOOK, type: EventType.CREATE_WEBHOOK;
metadata: { metadata: {
webhookId: string; webhookId: string;
environment: string; environment: string;
secretPath: string; secretPath: string;
webhookUrl: string; webhookUrl: string;
isDisabled: boolean; isDisabled: boolean;
} };
} }
interface UpdateWebhookStatusEvent { interface UpdateWebhookStatusEvent {
type: EventType.UPDATE_WEBHOOK_STATUS, type: EventType.UPDATE_WEBHOOK_STATUS;
metadata: { metadata: {
webhookId: string; webhookId: string;
environment: string; environment: string;
secretPath: string; secretPath: string;
webhookUrl: string; webhookUrl: string;
isDisabled: boolean; isDisabled: boolean;
} };
} }
interface DeleteWebhookEvent { interface DeleteWebhookEvent {
type: EventType.DELETE_WEBHOOK, type: EventType.DELETE_WEBHOOK;
metadata: { metadata: {
webhookId: string; webhookId: string;
environment: string; environment: string;
secretPath: string; secretPath: string;
webhookUrl: string; webhookUrl: string;
isDisabled: boolean; isDisabled: boolean;
} };
} }
interface GetSecretImportsEvent { interface GetSecretImportsEvent {
type: EventType.GET_SECRET_IMPORTS, type: EventType.GET_SECRET_IMPORTS;
metadata: { metadata: {
environment: string; environment: string;
secretImportId: string; secretImportId: string;
folderId: string; folderId: string;
numberOfImports: number; numberOfImports: number;
} };
} }
interface CreateSecretImportEvent { interface CreateSecretImportEvent {
type: EventType.CREATE_SECRET_IMPORT, type: EventType.CREATE_SECRET_IMPORT;
metadata: { metadata: {
secretImportId: string; secretImportId: string;
folderId: string; folderId: string;
importFromEnvironment: string; importFromEnvironment: string;
importFromSecretPath: string; importFromSecretPath: string;
importToEnvironment: string; importToEnvironment: string;
importToSecretPath: string; importToSecretPath: string;
} };
} }
interface UpdateSecretImportEvent { interface UpdateSecretImportEvent {
type: EventType.UPDATE_SECRET_IMPORT, type: EventType.UPDATE_SECRET_IMPORT;
metadata: { metadata: {
secretImportId: string; secretImportId: string;
folderId: string; folderId: string;
importToEnvironment: string; importToEnvironment: string;
importToSecretPath: string; importToSecretPath: string;
orderBefore: { orderBefore: {
environment: string; environment: string;
secretPath: string; secretPath: string;
}[], }[];
orderAfter: { orderAfter: {
environment: string; environment: string;
secretPath: string; secretPath: string;
}[] }[];
} };
} }
interface DeleteSecretImportEvent { interface DeleteSecretImportEvent {
type: EventType.DELETE_SECRET_IMPORT, type: EventType.DELETE_SECRET_IMPORT;
metadata: { metadata: {
secretImportId: string; secretImportId: string;
folderId: string; folderId: string;
importFromEnvironment: string; importFromEnvironment: string;
importFromSecretPath: string; importFromSecretPath: string;
importToEnvironment: string; importToEnvironment: string;
importToSecretPath: string; importToSecretPath: string;
} };
} }
interface UpdateUserRole { interface UpdateUserRole {
type: EventType.UPDATE_USER_WORKSPACE_ROLE, type: EventType.UPDATE_USER_WORKSPACE_ROLE;
metadata: { metadata: {
userId: string; userId: string;
email: string; email: string;
oldRole: string; oldRole: string;
newRole: string; newRole: string;
} };
} }
interface UpdateUserDeniedPermissions { interface UpdateUserDeniedPermissions {
type: EventType.UPDATE_USER_WORKSPACE_DENIED_PERMISSIONS, type: EventType.UPDATE_USER_WORKSPACE_DENIED_PERMISSIONS;
metadata: { metadata: {
userId: string; userId: string;
email: string; email: string;
deniedPermissions: { deniedPermissions: {
environmentSlug: string; environmentSlug: string;
ability: string; ability: string;
}[] }[];
} };
} }
export type Event =
export type Event = | GetSecretsEvent
| GetSecretsEvent | GetSecretEvent
| GetSecretEvent | CreateSecretEvent
| CreateSecretEvent | UpdateSecretEvent
| UpdateSecretEvent | DeleteSecretEvent
| DeleteSecretEvent | GetWorkspaceKeyEvent
| GetWorkspaceKeyEvent | AuthorizeIntegrationEvent
| AuthorizeIntegrationEvent | UnauthorizeIntegrationEvent
| UnauthorizeIntegrationEvent | CreateIntegrationEvent
| CreateIntegrationEvent | DeleteIntegrationEvent
| DeleteIntegrationEvent | AddTrustedIPEvent
| AddTrustedIPEvent | UpdateTrustedIPEvent
| UpdateTrustedIPEvent | DeleteTrustedIPEvent
| DeleteTrustedIPEvent | CreateServiceTokenEvent
| CreateServiceTokenEvent | DeleteServiceTokenEvent
| DeleteServiceTokenEvent | CreateServiceTokenV3Event
| CreateServiceTokenV3Event | UpdateServiceTokenV3Event
| UpdateServiceTokenV3Event | DeleteServiceTokenV3Event
| DeleteServiceTokenV3Event | CreateEnvironmentEvent
| CreateEnvironmentEvent | UpdateEnvironmentEvent
| UpdateEnvironmentEvent | DeleteEnvironmentEvent
| DeleteEnvironmentEvent | AddWorkspaceMemberEvent
| AddWorkspaceMemberEvent | RemoveWorkspaceMemberEvent
| RemoveWorkspaceMemberEvent | CreateFolderEvent
| CreateFolderEvent | UpdateFolderEvent
| UpdateFolderEvent | DeleteFolderEvent
| DeleteFolderEvent | CreateWebhookEvent
| CreateWebhookEvent | UpdateWebhookStatusEvent
| UpdateWebhookStatusEvent | DeleteWebhookEvent
| DeleteWebhookEvent | GetSecretImportsEvent
| GetSecretImportsEvent | CreateSecretImportEvent
| CreateSecretImportEvent | UpdateSecretImportEvent
| UpdateSecretImportEvent | DeleteSecretImportEvent
| DeleteSecretImportEvent | UpdateUserRole
| UpdateUserRole | UpdateUserDeniedPermissions;
| UpdateUserDeniedPermissions;
export type AuditLog = { export type AuditLog = {
_id: string; _id: string;
actor: Actor; actor: Actor;
organization: string; organization: string;
workspace: string; workspace: string;
ipAddress: string; ipAddress: string;
event: Event; event: Event;
@@ -454,14 +446,13 @@ export type AuditLog = {
userAgentType: UserAgentType; userAgentType: UserAgentType;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
} };
export type AuditLogFilters = { export type AuditLogFilters = {
eventType?: EventType; eventType?: EventType;
userAgentType?: UserAgentType; userAgentType?: UserAgentType;
actor?: string; actor?: string;
offset: number; limit: number;
limit: number; startDate?: Date;
startDate?: Date; endDate?: Date;
endDate?: Date; };
}
@@ -8,7 +8,7 @@ export const AuditLogsPage = withProjectPermission(
return ( return (
<div className="flex justify-center bg-bunker-800 text-white w-full h-full"> <div className="flex justify-center bg-bunker-800 text-white w-full h-full">
<div className="max-w-7xl px-6 w-full"> <div className="max-w-7xl px-6 w-full">
<div className="my-6"> <div className="py-6 sticky top-0 z-10 bg-bunker-800">
<p className="text-3xl font-semibold text-gray-200">Audit Logs</p> <p className="text-3xl font-semibold text-gray-200">Audit Logs</p>
<div /> <div />
</div> </div>
@@ -3,13 +3,7 @@ import { Control, Controller, UseFormReset } from "react-hook-form";
import { faFilterCircleXmark } from "@fortawesome/free-solid-svg-icons"; import { faFilterCircleXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { import { Button, DatePicker, FormControl, Select, SelectItem } from "@app/components/v2";
Button,
DatePicker,
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";
@@ -19,201 +13,207 @@ import { Actor } from "@app/hooks/api/auditLogs/types";
import { AuditLogFilterFormData } from "./types"; import { AuditLogFilterFormData } from "./types";
const eventTypes = Object.entries(eventToNameMap).map(([value, label]) => ({ label, value })); const eventTypes = Object.entries(eventToNameMap).map(([value, label]) => ({ label, value }));
const userAgentTypes = Object.entries(userAgentTTypeoNameMap).map(([value, label]) => ({ label, value })); const userAgentTypes = Object.entries(userAgentTTypeoNameMap).map(([value, label]) => ({
label,
value
}));
type Props = { type Props = {
control: Control<AuditLogFilterFormData>; control: Control<AuditLogFilterFormData>;
reset: UseFormReset<AuditLogFilterFormData>; reset: UseFormReset<AuditLogFilterFormData>;
} };
export const LogsFilter = ({ export const LogsFilter = ({ control, reset }: Props) => {
control, const [isStartDatePickerOpen, setIsStartDatePickerOpen] = useState(false);
reset const [isEndDatePickerOpen, setIsEndDatePickerOpen] = useState(false);
}: Props) => {
const [isStartDatePickerOpen, setIsStartDatePickerOpen] = useState(false);
const [isEndDatePickerOpen, setIsEndDatePickerOpen] = useState(false);
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const { data, isLoading } = useGetAuditLogActorFilterOpts(currentWorkspace?._id ?? ""); const { data, isLoading } = useGetAuditLogActorFilterOpts(currentWorkspace?._id ?? "");
const renderActorSelectItem = (actor: Actor) => {
switch (actor.type) {
case ActorType.USER:
return (
<SelectItem value={`${actor.type}-${actor.metadata.userId}`} key={`user-actor-filter-${actor.metadata.userId}`}>
{actor.metadata.email}
</SelectItem>
);
case ActorType.SERVICE:
return (
<SelectItem value={`${actor.type}-${actor.metadata.serviceId}`} key={`service-actor-filter-${actor.metadata.serviceId}`}>
{actor.metadata.name}
</SelectItem>
);
case ActorType.SERVICE_V3:
return (
<SelectItem value={`${actor.type}-${actor.metadata.serviceId}`} key={`service-actor-v3-filter-${actor.metadata.serviceId}`}>
{actor.metadata.name}
</SelectItem>
);
default:
return (
<SelectItem value="actor-none" key="actor-none">
N/A
</SelectItem>
);
}
const renderActorSelectItem = (actor: Actor) => {
switch (actor.type) {
case ActorType.USER:
return (
<SelectItem
value={`${actor.type}-${actor.metadata.userId}`}
key={`user-actor-filter-${actor.metadata.userId}`}
>
{actor.metadata.email}
</SelectItem>
);
case ActorType.SERVICE:
return (
<SelectItem
value={`${actor.type}-${actor.metadata.serviceId}`}
key={`service-actor-filter-${actor.metadata.serviceId}`}
>
{actor.metadata.name}
</SelectItem>
);
case ActorType.SERVICE_V3:
return (
<SelectItem
value={`${actor.type}-${actor.metadata.serviceId}`}
key={`service-actor-v3-filter-${actor.metadata.serviceId}`}
>
{actor.metadata.name}
</SelectItem>
);
default:
return (
<SelectItem value="actor-none" key="actor-none">
N/A
</SelectItem>
);
} }
};
return ( return (
<div className="flex justify-between items-center"> <div className="flex justify-between items-center sticky top-20 z-10 bg-bunker-800">
<div className="flex items-center"> <div className="flex items-center">
<Controller <Controller
control={control} control={control}
name="eventType" name="eventType"
render={({ field: { onChange, ...field }, fieldState: { error } }) => ( render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl <FormControl
label="Event" label="Event"
errorText={error?.message} errorText={error?.message}
isError={Boolean(error)} isError={Boolean(error)}
className="w-40 mr-4" className="w-40 mr-4"
> >
<Select <Select
{...(field.value ? { value: field.value } : { placeholder: "Select" })} {...(field.value ? { value: field.value } : { placeholder: "Select" })}
{...field} {...field}
onValueChange={(e) => onChange(e)} onValueChange={(e) => onChange(e)}
className="w-full bg-mineshaft-700 border border-mineshaft-500 text-mineshaft-100" className="w-full bg-mineshaft-700 border border-mineshaft-500 text-mineshaft-100"
> >
{eventTypes.map(({ label, value }) => ( {eventTypes.map(({ label, value }) => (
<SelectItem value={String(value || "")} key={label}> <SelectItem value={String(value || "")} key={label}>
{label} {label}
</SelectItem> </SelectItem>
))} ))}
</Select> </Select>
</FormControl> </FormControl>
)} )}
/> />
{!isLoading && data && data.length > 0 && ( {!isLoading && data && data.length > 0 && (
<Controller <Controller
control={control} control={control}
name="actor" name="actor"
render={({ field: { onChange, ...field }, fieldState: { error } }) => ( render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl <FormControl
label="Actor" label="Actor"
errorText={error?.message} errorText={error?.message}
isError={Boolean(error)} isError={Boolean(error)}
className="w-40 mr-4" className="w-40 mr-4"
> >
<Select <Select
{...(field.value ? { value: field.value } : { placeholder: "Select" })} {...(field.value ? { value: field.value } : { placeholder: "Select" })}
{...field} {...field}
onValueChange={(e) => onChange(e)} onValueChange={(e) => onChange(e)}
className="w-full bg-mineshaft-700 border border-mineshaft-500 text-mineshaft-100" className="w-full bg-mineshaft-700 border border-mineshaft-500 text-mineshaft-100"
>
{data.map((actor) => renderActorSelectItem(actor))}
</Select>
</FormControl>
)}
/>
)}
<Controller
control={control}
name="userAgentType"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Source"
errorText={error?.message}
isError={Boolean(error)}
className="w-40 mr-4"
>
<Select
{...(field.value ? { value: field.value } : { placeholder: "Select" })}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full bg-mineshaft-700 border border-mineshaft-500 text-mineshaft-100"
>
{userAgentTypes.map(({ label, value }) => (
<SelectItem value={String(value || "")} key={label}>
{label}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<Controller
name="startDate"
control={control}
render={({ field: { onChange, ...field }, fieldState: { error } }) => {
return (
<FormControl
label="Start date"
errorText={error?.message}
isError={Boolean(error)}
className="mr-4"
>
<DatePicker
value={field.value || undefined}
onChange={date => {
onChange(date);
setIsStartDatePickerOpen(false);
}}
popUpProps={{
open: isStartDatePickerOpen,
onOpenChange: setIsStartDatePickerOpen
}}
popUpContentProps={{}}
/>
</FormControl>
);
}}
/>
<Controller
name="endDate"
control={control}
render={({ field: { onChange, ...field }, fieldState: { error } }) => {
return (
<FormControl
label="End date"
errorText={error?.message}
isError={Boolean(error)}
>
<DatePicker
value={field.value || undefined}
onChange={date => {
onChange(date);
setIsEndDatePickerOpen(false);
}}
popUpProps={{
open: isEndDatePickerOpen,
onOpenChange: setIsEndDatePickerOpen
}}
popUpContentProps={{}}
/>
</FormControl>
);
}}
/>
</div>
<div>
<Button
isLoading={false}
colorSchema="primary"
variant="outline_bg"
type="submit"
leftIcon={<FontAwesomeIcon icon={faFilterCircleXmark} className="mr-2" />}
onClick={() => reset({
eventType: undefined,
actor: undefined,
userAgentType: undefined,
startDate: undefined,
endDate: undefined
})}
> >
Clear filters {data.map((actor) => renderActorSelectItem(actor))}
</Button> </Select>
</div> </FormControl>
</div> )}
); />
} )}
<Controller
control={control}
name="userAgentType"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Source"
errorText={error?.message}
isError={Boolean(error)}
className="w-40 mr-4"
>
<Select
{...(field.value ? { value: field.value } : { placeholder: "Select" })}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full bg-mineshaft-700 border border-mineshaft-500 text-mineshaft-100"
>
{userAgentTypes.map(({ label, value }) => (
<SelectItem value={String(value || "")} key={label}>
{label}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<Controller
name="startDate"
control={control}
render={({ field: { onChange, ...field }, fieldState: { error } }) => {
return (
<FormControl
label="Start date"
errorText={error?.message}
isError={Boolean(error)}
className="mr-4"
>
<DatePicker
value={field.value || undefined}
onChange={(date) => {
onChange(date);
setIsStartDatePickerOpen(false);
}}
popUpProps={{
open: isStartDatePickerOpen,
onOpenChange: setIsStartDatePickerOpen
}}
popUpContentProps={{}}
/>
</FormControl>
);
}}
/>
<Controller
name="endDate"
control={control}
render={({ field: { onChange, ...field }, fieldState: { error } }) => {
return (
<FormControl label="End date" errorText={error?.message} isError={Boolean(error)}>
<DatePicker
value={field.value || undefined}
onChange={(date) => {
onChange(date);
setIsEndDatePickerOpen(false);
}}
popUpProps={{
open: isEndDatePickerOpen,
onOpenChange: setIsEndDatePickerOpen
}}
popUpContentProps={{}}
/>
</FormControl>
);
}}
/>
</div>
<div>
<Button
isLoading={false}
colorSchema="primary"
variant="outline_bg"
type="submit"
leftIcon={<FontAwesomeIcon icon={faFilterCircleXmark} className="mr-2" />}
onClick={() =>
reset({
eventType: undefined,
actor: undefined,
userAgentType: undefined,
startDate: undefined,
endDate: undefined
})
}
>
Clear filters
</Button>
</div>
</div>
);
};
@@ -13,78 +13,54 @@ import { LogsTable } from "./LogsTable";
import { AuditLogFilterFormData, auditLogFilterFormSchema } from "./types"; import { AuditLogFilterFormData, auditLogFilterFormSchema } from "./types";
export const LogsSection = () => { export const LogsSection = () => {
const { subscription } = useSubscription(); const { subscription } = useSubscription();
const router = useRouter(); const router = useRouter();
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
"upgradePlan"
] as const);
const {
control,
reset,
watch,
setValue,
} = useForm<AuditLogFilterFormData>({
resolver: yupResolver(auditLogFilterFormSchema),
defaultValues: {
page: 1,
perPage: 10
}
});
useEffect(() => {
if (subscription && !subscription.auditLogs) {
handlePopUpOpen("upgradePlan");
}
}, [subscription]);
const eventType = watch("eventType") as EventType | undefined; const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["upgradePlan"] as const);
const userAgentType = watch("userAgentType") as UserAgentType | undefined;
const actor = watch("actor");
const startDate = watch("startDate"); const { control, reset, watch } = useForm<AuditLogFilterFormData>({
const endDate = watch("endDate"); resolver: yupResolver(auditLogFilterFormSchema),
defaultValues: {
page: 1,
perPage: 10
}
});
const page = watch("page") as number; useEffect(() => {
const perPage = watch("perPage") as number; if (subscription && !subscription.auditLogs) {
handlePopUpOpen("upgradePlan");
return ( }
<div }, [subscription]);
// className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600"
>
{/* <div className="flex items-center mb-8">
<h2 className="text-xl font-semibold flex-1 text-white">
Audit Logs
</h2>
</div> */}
<LogsFilter
control={control}
reset={reset}
/>
<LogsTable
eventType={eventType}
userAgentType={userAgentType}
actor={actor}
startDate={startDate}
endDate={endDate}
page={page}
perPage={perPage}
setValue={setValue}
/>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => {
if (!isOpen) {
router.back();
return;
}
handlePopUpToggle("upgradePlan", isOpen) const eventType = watch("eventType") as EventType | undefined;
}} const userAgentType = watch("userAgentType") as UserAgentType | undefined;
text="You can use audit logs if you switch to a paid Infisical plan." const actor = watch("actor");
/>
</div> const startDate = watch("startDate");
); const endDate = watch("endDate");
}
return (
<div>
<LogsFilter control={control} reset={reset} />
<LogsTable
eventType={eventType}
userAgentType={userAgentType}
actor={actor}
startDate={startDate}
endDate={endDate}
/>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => {
if (!isOpen) {
router.back();
return;
}
handlePopUpToggle("upgradePlan", isOpen);
}}
text="You can use audit logs if you switch to a paid Infisical plan."
/>
</div>
);
};
@@ -1,96 +1,95 @@
import { Fragment } from "react";
import { faFile } from "@fortawesome/free-solid-svg-icons"; import { faFile } from "@fortawesome/free-solid-svg-icons";
import { import {
EmptyState, Button,
Pagination, EmptyState,
Table, Table,
TableContainer, TableContainer,
TableSkeleton, TableSkeleton,
TBody, TBody,
Td, Td,
Th, Th,
THead, THead,
Tr} from "@app/components/v2"; Tr
} 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 { EventType, UserAgentType } from "@app/hooks/api/auditLogs/enums";
import { LogsTableRow } from "./LogsTableRow"; import { LogsTableRow } from "./LogsTableRow";
import { SetValueType } from "./types";
type Props = { type Props = {
eventType?: EventType; eventType?: EventType;
userAgentType?: UserAgentType; userAgentType?: UserAgentType;
actor?: string; actor?: string;
startDate?: Date; startDate?: Date;
endDate?: Date; endDate?: Date;
page: number; };
perPage: number;
setValue: SetValueType;
}
export const LogsTable = ({ const AUDIT_LOG_LIMIT = 15;
eventType,
userAgentType, export const LogsTable = ({ eventType, userAgentType, actor, startDate, endDate }: Props) => {
actor, const { currentWorkspace } = useWorkspace();
startDate, const { data, isLoading, isFetchingNextPage, hasNextPage, fetchNextPage } = useGetAuditLogs(
endDate, currentWorkspace?._id ?? "",
page, {
perPage, eventType,
setValue userAgentType,
}: Props) => { actor,
const { currentWorkspace } = useWorkspace(); startDate,
const { data, isLoading } = useGetAuditLogs(currentWorkspace?._id ?? "", { endDate,
eventType, limit: AUDIT_LOG_LIMIT
userAgentType, }
actor, );
startDate,
endDate, const isEmpty = !isLoading && !data?.pages?.[0].length;
offset: (page - 1) * perPage,
limit: perPage return (
}); <div>
<TableContainer>
return ( <Table>
<TableContainer> <THead>
<Table> <Tr>
<THead> <Th>Timestamp</Th>
<Tr> <Th>Event</Th>
<Th>Timestamp</Th> <Th>Actor</Th>
<Th>Event</Th> <Th>Source</Th>
<Th>Actor</Th> <Th>Metadata</Th>
<Th>Source</Th> </Tr>
<Th>Metadata</Th> </THead>
</Tr> <TBody>
</THead> {!isLoading &&
<TBody> data?.pages?.map((group, i) => (
{!isLoading && data?.auditLogs && data?.auditLogs.map((auditLog) => ( <Fragment key={`auditlog-item-${i + 1}`}>
<LogsTableRow {group.map((auditLog) => (
auditLog={auditLog} <LogsTableRow auditLog={auditLog} key={`audit-log-${auditLog._id}`} />
key={`audit-log-${auditLog._id}`} ))}
/> </Fragment>
))} ))}
{isLoading && <TableSkeleton innerKey="logs-table" columns={5} key="logs" />} {isLoading && <TableSkeleton innerKey="logs-table" columns={5} key="logs" />}
{!isLoading && data?.auditLogs && data?.auditLogs.length === 0 && ( {isEmpty && (
<Tr> <Tr>
<Td colSpan={5}> <Td colSpan={5}>
<EmptyState <EmptyState title="No audit logs on file" icon={faFile} />
title="No audit logs on file" </Td>
icon={faFile} </Tr>
/>
</Td>
</Tr>
)}
</TBody>
</Table>
{!isLoading && data?.totalCount !== undefined && (
<Pagination
count={data?.totalCount}
page={page}
perPage={perPage}
onChangePage={(newPage) => setValue("page", newPage)}
onChangePerPage={(newPerPage) => setValue("perPage", newPerPage)}
/>
)} )}
</TableContainer> </TBody>
); </Table>
} </TableContainer>
{!isEmpty && (
<Button
className="mt-4 mb-20 py-3 px-4 text-sm"
isFullWidth
variant="star"
isLoading={isFetchingNextPage}
isDisabled={isFetchingNextPage || !hasNextPage}
onClick={() => fetchNextPage()}
>
{hasNextPage ? "Load More" : "End of logs"}
</Button>
)}
</div>
);
};