From 6759734ce22ddfe63fad7dc70cc9bf4620ddaf49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Zori=C4=87?= Date: Tue, 14 Jan 2025 12:33:19 +0100 Subject: [PATCH] fix(api-log): handle error on log insert (#4488) --- .../api-log/__tests__/mocks/getIdentity.ts | 9 ++ .../tasks/pruneLogs/PruneLogs.test.ts | 27 ++++ packages/api-log/package.json | 2 + packages/api-log/src/context.ts | 4 +- packages/api-log/src/crud/index.ts | 50 +++++++- packages/api-log/src/graphql/index.ts | 12 +- packages/api-log/src/graphql/plugin.ts | 71 ++++++++--- packages/api-log/src/logger/factory.ts | 12 +- packages/api-log/src/tasks/constants.ts | 1 + .../api-log/src/tasks/createPruneLogsTask.ts | 6 +- .../api-log/src/tasks/pruneLogs/PruneLogs.ts | 29 ++++- packages/api-log/src/tasks/pruneLogs/types.ts | 2 + packages/api-log/src/types.ts | 15 ++- packages/api-log/src/utils/storeKey.ts | 5 + packages/api-log/tsconfig.build.json | 2 + packages/api-log/tsconfig.json | 6 + .../__tests__/handlers/graphQlHandler.ts | 1 + .../__tests__/handlers/graphql/index.ts | 4 +- .../__tests__/handlers/graphql/logger.ts | 115 ++++++++++++++++++ .../__tests__/handlers/helpers/core.ts | 5 +- .../handlers/helpers/tenancySecurity.ts | 3 + .../__tests__/handlers/logger/logger.test.ts | 103 ++++++++++++++++ packages/api-serverless-cms/package.json | 2 +- packages/api-serverless-cms/src/index.ts | 4 +- .../api-serverless-cms/tsconfig.build.json | 2 +- packages/api-serverless-cms/tsconfig.json | 6 +- .../src/apps/api/ApiApwScheduler.ts | 14 +++ .../pulumi-aws/src/apps/api/ApiFileManager.ts | 14 +++ .../pulumi-aws/src/apps/api/ApiGraphql.ts | 2 + .../pulumi-aws/src/apps/api/ApiPageBuilder.ts | 4 +- .../src/apps/website/WebsitePrerendering.ts | 4 +- yarn.lock | 2 + 32 files changed, 499 insertions(+), 39 deletions(-) create mode 100644 packages/api-log/__tests__/mocks/getIdentity.ts create mode 100644 packages/api-log/src/tasks/constants.ts create mode 100644 packages/api-log/src/utils/storeKey.ts create mode 100644 packages/api-serverless-cms/__tests__/handlers/graphql/logger.ts create mode 100644 packages/api-serverless-cms/__tests__/handlers/logger/logger.test.ts diff --git a/packages/api-log/__tests__/mocks/getIdentity.ts b/packages/api-log/__tests__/mocks/getIdentity.ts new file mode 100644 index 00000000000..fdb1a2c6b49 --- /dev/null +++ b/packages/api-log/__tests__/mocks/getIdentity.ts @@ -0,0 +1,9 @@ +import { SecurityIdentity } from "@webiny/api-security/types"; + +export const getIdentity = (): Pick => { + return { + id: "mocked-identity-id", + displayName: "mocked-identity-display-name", + type: "mocked-identity-type" + }; +}; diff --git a/packages/api-log/__tests__/tasks/pruneLogs/PruneLogs.test.ts b/packages/api-log/__tests__/tasks/pruneLogs/PruneLogs.test.ts index 15b8898f9c2..12e07a33d30 100644 --- a/packages/api-log/__tests__/tasks/pruneLogs/PruneLogs.test.ts +++ b/packages/api-log/__tests__/tasks/pruneLogs/PruneLogs.test.ts @@ -12,6 +12,8 @@ import { import { Entity } from "@webiny/db-dynamodb/toolbox"; import { create } from "~/db"; import { createMockLogger } from "~tests/mocks/logger"; +import { GetValueResult, IStore, RemoveValueResult, StorageKey } from "@webiny/db"; +import { getIdentity } from "~tests/mocks/getIdentity"; describe("PruneLogs", () => { let prune: PruneLogs; @@ -23,6 +25,27 @@ describe("PruneLogs", () => { let logger: ILogger; + const store: Pick = { + async getValue(key: StorageKey): Promise> { + return { + key, + data: { + identity: getIdentity(), + taskId: "1234" + } + }; + }, + async removeValue(key: StorageKey): Promise> { + return { + key, + data: { + identity: getIdentity(), + taskId: "1234" + } + }; + } + }; + beforeEach(async () => { prune = new PruneLogs({ documentClient, @@ -64,6 +87,7 @@ describe("PruneLogs", () => { }; }; const result = await prune.execute({ + store, list, response, input: {}, @@ -186,6 +210,7 @@ describe("PruneLogs", () => { * Should not prune anything because the default date is too far into the past. */ const pruneNothingResult = await prune.execute({ + store, list, response, input: {}, @@ -209,6 +234,7 @@ describe("PruneLogs", () => { * Only prune from anotherTenant. */ const pruneResult = await prune.execute({ + store, list, response, input: { @@ -256,6 +282,7 @@ describe("PruneLogs", () => { * And then prune everything. */ const pruneAllResult = await prune.execute({ + store, list, response, input: { diff --git a/packages/api-log/package.json b/packages/api-log/package.json index 08f6c66a305..721cbc258dc 100644 --- a/packages/api-log/package.json +++ b/packages/api-log/package.json @@ -19,7 +19,9 @@ "@webiny/api-security": "0.0.0", "@webiny/api-tenancy": "0.0.0", "@webiny/aws-sdk": "0.0.0", + "@webiny/db": "0.0.0", "@webiny/db-dynamodb": "0.0.0", + "@webiny/error": "0.0.0", "@webiny/handler": "0.0.0", "@webiny/handler-graphql": "0.0.0", "@webiny/plugins": "0.0.0", diff --git a/packages/api-log/src/context.ts b/packages/api-log/src/context.ts index b27254dac42..5aed39de8a6 100644 --- a/packages/api-log/src/context.ts +++ b/packages/api-log/src/context.ts @@ -10,6 +10,7 @@ export interface ICreateLoggerContextParams { documentClient?: DynamoDBDocument; getTenant?: () => string; getLocale?: () => string; + createGraphQL?: boolean; } const getDocumentClient = (context: Context) => { @@ -55,11 +56,12 @@ export const createContextPlugin = (params?: ICreateLoggerContextParams) => { context.logger = { log: logger, ...createCrud({ + getContext, storageOperations, checkPermission: checkPermissionFactory({ getContext }) }) }; - context.plugins.register(createGraphQl()); + context.plugins.register(createGraphQl(params)); }); plugin.name = "logger.createContext"; diff --git a/packages/api-log/src/crud/index.ts b/packages/api-log/src/crud/index.ts index 0ebff2bd64e..22890fac744 100644 --- a/packages/api-log/src/crud/index.ts +++ b/packages/api-log/src/crud/index.ts @@ -9,18 +9,24 @@ import { ILoggerCrudListLogsParams, ILoggerCrudListLogsResponse, ILoggerLog, + ILoggerPruneLogsResponse, ILoggerStorageOperations, - ILoggerWithSource + ILoggerWithSource, + IPruneLogsStoredValue } from "~/types"; import { NotFoundError } from "@webiny/handler-graphql"; +import { WebinyError } from "@webiny/error"; +import { PRUNE_LOGS_TASK } from "~/tasks/constants"; +import { createStoreKey } from "~/utils/storeKey"; export interface ICreateCrudParams { + getContext: () => Pick; storageOperations: ILoggerStorageOperations; checkPermission(): Promise; } export const createCrud = (params: ICreateCrudParams): ILoggerCrud => { - const { storageOperations, checkPermission } = params; + const { storageOperations, checkPermission, getContext } = params; return { async getLog(params: ILoggerCrudGetLogsParams): Promise { @@ -78,6 +84,46 @@ export const createCrud = (params: ICreateCrudParams): ILoggerCrud => { return this.log.flush(); } }; + }, + async pruneLogs(): Promise { + await checkPermission(); + + const context = getContext(); + + const key = createStoreKey(); + + const alreadyPruning = await context.db.store.getValue(key); + + if (alreadyPruning?.data?.taskId) { + throw new WebinyError({ + message: "Already pruning logs.", + code: "ALREADY_PRUNING_LOGS", + data: { + identity: alreadyPruning.data.identity, + taskId: alreadyPruning.data.taskId + } + }); + } + + const task = await context.tasks.trigger({ + definition: PRUNE_LOGS_TASK, + name: "Prune all Webiny logs" + }); + + const identity = context.security.getIdentity(); + + await context.db.store.storeValue(key, { + identity: { + id: identity.id, + displayName: identity.displayName, + type: identity.type + }, + taskId: task.id + }); + + return { + task + }; } }; }; diff --git a/packages/api-log/src/graphql/index.ts b/packages/api-log/src/graphql/index.ts index 6d630d4ba8a..68e450c9d25 100644 --- a/packages/api-log/src/graphql/index.ts +++ b/packages/api-log/src/graphql/index.ts @@ -1,8 +1,16 @@ import { Plugin } from "@webiny/plugins/types"; import { createGraphQlPlugin } from "~/graphql/plugin"; -export const createGraphQl = (): Plugin[] => { - if (process.env.DEBUG !== "true") { +export interface ICreateGraphQlParams { + createGraphQL?: boolean; +} + +export const createGraphQl = (params?: ICreateGraphQlParams): Plugin[] => { + /** + * If the `createGraphQl` flag is set to `true` or debug mode is enabled, we'll create the GraphQL plugin. + */ + const debug = [params?.createGraphQL === true, process.env.DEBUG === "true"].some(Boolean); + if (!debug) { return []; } diff --git a/packages/api-log/src/graphql/plugin.ts b/packages/api-log/src/graphql/plugin.ts index 29b1d8df9da..052adba84f4 100644 --- a/packages/api-log/src/graphql/plugin.ts +++ b/packages/api-log/src/graphql/plugin.ts @@ -3,6 +3,8 @@ import { Context, LogType } from "~/types"; import zod from "zod"; import { createZodError } from "@webiny/utils"; +export const emptyResolver = () => ({}); + const getLogArgsSchema = zod.object({ where: zod.object({ id: zod.string() @@ -10,13 +12,15 @@ const getLogArgsSchema = zod.object({ }); const listLogsArgsSchema = zod.object({ - where: zod.object({ - tenant: zod.string().optional(), - source: zod.string().optional(), - type: zod - .enum([LogType.DEBUG, LogType.NOTICE, LogType.INFO, LogType.WARN, LogType.ERROR]) - .optional() - }), + where: zod + .object({ + tenant: zod.string().optional(), + source: zod.string().optional(), + type: zod + .enum([LogType.DEBUG, LogType.NOTICE, LogType.INFO, LogType.WARN, LogType.ERROR]) + .optional() + }) + .optional(), sort: zod.array(zod.enum(["ASC", "DESC"])).optional(), limit: zod.number().optional(), after: zod.string().optional() @@ -39,14 +43,22 @@ const deleteLogsArgsSchema = zod.object({ export const createGraphQlPlugin = () => { return new GraphQLSchemaPlugin({ typeDefs: /* GraphQL */ ` + type LogsQuery { + _empty: String + } + + type LogsMutation { + _empty: String + } + extend type Query { - log: LogQuery + logs: LogsQuery } extend type Mutation { - log: LogMutation + logs: LogsMutation } - + enum LogType { ${LogType.DEBUG} ${LogType.NOTICE} @@ -92,14 +104,18 @@ export const createGraphQlPlugin = () => { source: String type: LogType } + + input GetLogWhereInput { + id: ID! + } enum ListLogsSortEnum { ASC DESC } - type LogQuery { - getLog(id: ID!): LogQueryGetResponse! + extend type LogsQuery { + getLog(where: GetLogWhereInput!): LogQueryGetResponse! listLogs( where: ListLogsWhereInput sort: ListLogsSortEnum @@ -118,6 +134,19 @@ export const createGraphQlPlugin = () => { error: LogQueryResponseError } + type LogMutationPruneLogsResponseDataTask { + id: ID! + } + + type LogMutationPruneLogsResponseData { + task: LogMutationPruneLogsResponseDataTask! + } + + type LogMutationPruneLogsResponse { + data: LogMutationPruneLogsResponseData + error: LogQueryResponseError + } + input DeleteLogWhereInput { id: ID! } @@ -126,13 +155,20 @@ export const createGraphQlPlugin = () => { items: [ID!]! } - type LogMutation { + extend type LogsMutation { + pruneLogs: LogMutationPruneLogsResponse! deleteLog(where: DeleteLogWhereInput!): LogMutationDeleteLogResponse! deleteLogs(where: DeleteLogsWhereInput!): LogMutationDeleteLogsResponse! } `, resolvers: { - LogQuery: { + Query: { + logs: emptyResolver + }, + Mutation: { + logs: emptyResolver + }, + LogsQuery: { getLog: async (_: unknown, args: unknown, context) => { return resolve(async () => { const result = getLogArgsSchema.safeParse(args); @@ -152,7 +188,7 @@ export const createGraphQlPlugin = () => { }); } }, - LogMutation: { + LogsMutation: { deleteLog: async (_, args, context) => { return resolve(async () => { const result = deleteLogArgsSchema.safeParse(args); @@ -170,6 +206,11 @@ export const createGraphQlPlugin = () => { } return await context.logger.deleteLogs(result.data); }); + }, + pruneLogs: async (_: unknown, __: unknown, context) => { + return resolve(async () => { + return await context.logger.pruneLogs(); + }); } } } diff --git a/packages/api-log/src/logger/factory.ts b/packages/api-log/src/logger/factory.ts index 80f0a6a3465..a4179836c95 100644 --- a/packages/api-log/src/logger/factory.ts +++ b/packages/api-log/src/logger/factory.ts @@ -22,9 +22,15 @@ export const loggerFactory = ({ getTenant, getLocale, documentClient }: ILoggerF return { logger: createDynamoDbLogger({ onFlush: async items => { - return await storageOperations.insert({ - items - }); + try { + return await storageOperations.insert({ + items + }); + } catch (ex) { + console.error("Error flushing logs."); + console.log(ex); + } + return []; }, getLocale, getTenant diff --git a/packages/api-log/src/tasks/constants.ts b/packages/api-log/src/tasks/constants.ts new file mode 100644 index 00000000000..d3dc502bfb3 --- /dev/null +++ b/packages/api-log/src/tasks/constants.ts @@ -0,0 +1 @@ +export const PRUNE_LOGS_TASK = "pruneLogs"; diff --git a/packages/api-log/src/tasks/createPruneLogsTask.ts b/packages/api-log/src/tasks/createPruneLogsTask.ts index 52f5bf214fe..f2cf47aa872 100644 --- a/packages/api-log/src/tasks/createPruneLogsTask.ts +++ b/packages/api-log/src/tasks/createPruneLogsTask.ts @@ -2,8 +2,7 @@ import { createTaskDefinition } from "@webiny/tasks"; import { Context, IPruneLogsInput, IPruneLogsOutput } from "~/tasks/pruneLogs/types"; import { LogType } from "~/types"; import { NonEmptyArray } from "@webiny/api/types"; - -export const PRUNE_LOGS_TASK = "pruneLogs"; +import { PRUNE_LOGS_TASK } from "./constants"; export const createPruneLogsTask = () => { return createTaskDefinition({ @@ -29,6 +28,7 @@ export const createPruneLogsTask = () => { keys: new DynamoDbLoggerKeys() }); return await prune.execute({ + store: params.context.db.store, input: params.input, list: params.context.logger.listLogs, response: params.response, @@ -48,7 +48,7 @@ export const createPruneLogsTask = () => { return { tenant: validator.string().optional(), source: validator.string().optional(), - keys: validator.object({}).passthrough(), + keys: validator.object({}).passthrough().optional(), type: validator.enum(Object.keys(LogType) as NonEmptyArray).optional(), createdAfter: validator .string() diff --git a/packages/api-log/src/tasks/pruneLogs/PruneLogs.ts b/packages/api-log/src/tasks/pruneLogs/PruneLogs.ts index 7e06aa23828..8fef9498391 100644 --- a/packages/api-log/src/tasks/pruneLogs/PruneLogs.ts +++ b/packages/api-log/src/tasks/pruneLogs/PruneLogs.ts @@ -1,10 +1,17 @@ import { ITaskResponse, ITaskResponseResult } from "@webiny/tasks"; import { IPruneLogsInput, IPruneLogsOutput } from "~/tasks/pruneLogs/types"; import { create } from "~/db"; -import { ILoggerCrudListLogsCallable, ILoggerCrudListLogsResponse, ILoggerLog } from "~/types"; +import { + ILoggerCrudListLogsCallable, + ILoggerCrudListLogsResponse, + ILoggerLog, + IPruneLogsStoredValue +} from "~/types"; import { batchWriteAll } from "@webiny/db-dynamodb"; import { DynamoDbLoggerKeys } from "~/logger"; import { DynamoDBDocument } from "@webiny/aws-sdk/client-dynamodb"; +import { IStore } from "@webiny/db"; +import { createStoreKey } from "~/utils/storeKey"; const getDate = (input: string | undefined, reduceSeconds = 60): Date => { if (input) { @@ -24,6 +31,7 @@ export interface IPruneLogsExecuteParams< I extends IPruneLogsInput = IPruneLogsInput, O extends IPruneLogsOutput = IPruneLogsOutput > { + store: Pick; list: ILoggerCrudListLogsCallable; input: I; response: ITaskResponse; @@ -44,7 +52,7 @@ export class PruneLogs< } public async execute(params: IPruneLogsExecuteParams): Promise { - const { list, response, input, isAborted, isCloseToTimeout } = params; + const { list, response, input, isAborted, isCloseToTimeout, store } = params; const { entity, table } = create({ documentClient: this.documentClient @@ -113,9 +121,26 @@ export class PruneLogs< startKey = result.meta.cursor || undefined; } } while (startKey); + const output: IPruneLogsOutput = { items: totalItems }; + + const key = createStoreKey(); + const stored = await store.getValue(key); + if (!stored.data?.taskId) { + return response.done(output as O); + } + try { + await store.removeValue(key); + } catch { + return response.done({ + ...output, + message: "Failed to remove the stored value. Please remove it manually.", + key + } as O); + } + return response.done(output as O); } } diff --git a/packages/api-log/src/tasks/pruneLogs/types.ts b/packages/api-log/src/tasks/pruneLogs/types.ts index 95bb5d593c5..ed8a82f38f6 100644 --- a/packages/api-log/src/tasks/pruneLogs/types.ts +++ b/packages/api-log/src/tasks/pruneLogs/types.ts @@ -12,6 +12,8 @@ export interface IPruneLogsInput { export interface IPruneLogsOutput extends ITaskResponseDoneResultOutput { items: number; + message?: string; + key?: string; } export interface Context extends LoggerContext, TaskContext {} diff --git a/packages/api-log/src/types.ts b/packages/api-log/src/types.ts index 9efaacc77e4..03054a84996 100644 --- a/packages/api-log/src/types.ts +++ b/packages/api-log/src/types.ts @@ -1,6 +1,8 @@ import { TenancyContext } from "@webiny/api-tenancy/types"; import { I18NContext } from "@webiny/api-i18n/types"; import { Context as HandlerContext } from "@webiny/handler/types"; +import { Context as TasksContext, ITask } from "@webiny/tasks/types"; +import { SecurityIdentity } from "@webiny/api-security/types"; export interface ILoggerLogCallableOptions { tenant?: string; @@ -104,12 +106,17 @@ export interface ILoggerCrudListLogsCallable { (params: ILoggerCrudListLogsParams): Promise; } +export interface ILoggerPruneLogsResponse { + task: ITask; +} + export interface ILoggerCrud { withSource(source: string): ILoggerWithSource; listLogs: ILoggerCrudListLogsCallable; getLog(params: ILoggerCrudGetLogsParams): Promise; deleteLog(params: ILoggerCrudDeleteLogParams): Promise; deleteLogs(params: ILoggerCrudDeleteLogsParams): Promise; + pruneLogs(): Promise; } export interface ILoggerStorageOperationsInsertParams { @@ -138,10 +145,16 @@ export interface ILogger { } export interface Context - extends Pick, + extends Pick, Pick, + Pick, HandlerContext { logger: ILoggerCrud & { log: ILogger; }; } + +export interface IPruneLogsStoredValue { + identity: Pick; + taskId: string; +} diff --git a/packages/api-log/src/utils/storeKey.ts b/packages/api-log/src/utils/storeKey.ts new file mode 100644 index 00000000000..3fb4df1884b --- /dev/null +++ b/packages/api-log/src/utils/storeKey.ts @@ -0,0 +1,5 @@ +import { StorageKey } from "@webiny/db"; + +export const createStoreKey = (): StorageKey => { + return `logs#prune`; +}; diff --git a/packages/api-log/tsconfig.build.json b/packages/api-log/tsconfig.build.json index 7948a6dddf4..88af5a9e39b 100644 --- a/packages/api-log/tsconfig.build.json +++ b/packages/api-log/tsconfig.build.json @@ -7,7 +7,9 @@ { "path": "../api-security/tsconfig.build.json" }, { "path": "../api-tenancy/tsconfig.build.json" }, { "path": "../aws-sdk/tsconfig.build.json" }, + { "path": "../db/tsconfig.build.json" }, { "path": "../db-dynamodb/tsconfig.build.json" }, + { "path": "../error/tsconfig.build.json" }, { "path": "../handler/tsconfig.build.json" }, { "path": "../handler-graphql/tsconfig.build.json" }, { "path": "../plugins/tsconfig.build.json" }, diff --git a/packages/api-log/tsconfig.json b/packages/api-log/tsconfig.json index 87fd73d93dc..2b03e1a1dd8 100644 --- a/packages/api-log/tsconfig.json +++ b/packages/api-log/tsconfig.json @@ -7,7 +7,9 @@ { "path": "../api-security" }, { "path": "../api-tenancy" }, { "path": "../aws-sdk" }, + { "path": "../db" }, { "path": "../db-dynamodb" }, + { "path": "../error" }, { "path": "../handler" }, { "path": "../handler-graphql" }, { "path": "../plugins" }, @@ -31,8 +33,12 @@ "@webiny/api-tenancy": ["../api-tenancy/src"], "@webiny/aws-sdk/*": ["../aws-sdk/src/*"], "@webiny/aws-sdk": ["../aws-sdk/src"], + "@webiny/db/*": ["../db/src/*"], + "@webiny/db": ["../db/src"], "@webiny/db-dynamodb/*": ["../db-dynamodb/src/*"], "@webiny/db-dynamodb": ["../db-dynamodb/src"], + "@webiny/error/*": ["../error/src/*"], + "@webiny/error": ["../error/src"], "@webiny/handler/*": ["../handler/src/*"], "@webiny/handler": ["../handler/src"], "@webiny/handler-graphql/*": ["../handler-graphql/src/*"], diff --git a/packages/api-serverless-cms/__tests__/handlers/graphQlHandler.ts b/packages/api-serverless-cms/__tests__/handlers/graphQlHandler.ts index 3c1f7354ad8..faf845cbed1 100644 --- a/packages/api-serverless-cms/__tests__/handlers/graphQlHandler.ts +++ b/packages/api-serverless-cms/__tests__/handlers/graphQlHandler.ts @@ -55,6 +55,7 @@ export const useGraphQlHandler = (params: IGraphQlHandlerParams) => { securityStorage: core.securityStorage, tenancyStorage: core.tenancyStorage, login: core.login, + logout: core.logout, ...createGraphQl({ createQuery, createMutation diff --git a/packages/api-serverless-cms/__tests__/handlers/graphql/index.ts b/packages/api-serverless-cms/__tests__/handlers/graphql/index.ts index c4a7c3dd941..da1949bf83a 100644 --- a/packages/api-serverless-cms/__tests__/handlers/graphql/index.ts +++ b/packages/api-serverless-cms/__tests__/handlers/graphql/index.ts @@ -1,5 +1,6 @@ import { createInstallGraphQL } from "./install"; import { ICreateMutationCb, ICreateQueryCb } from "~tests/handlers/helpers/factory/types"; +import { createLoggerGraphQL } from "./logger"; export interface ICreateGraphQlParams { createQuery: ICreateQueryCb; @@ -8,6 +9,7 @@ export interface ICreateGraphQlParams { export const createGraphQl = (params: ICreateGraphQlParams) => { return { - ...createInstallGraphQL(params) + ...createInstallGraphQL(params), + ...createLoggerGraphQL(params) }; }; diff --git a/packages/api-serverless-cms/__tests__/handlers/graphql/logger.ts b/packages/api-serverless-cms/__tests__/handlers/graphql/logger.ts new file mode 100644 index 00000000000..2903cf0d253 --- /dev/null +++ b/packages/api-serverless-cms/__tests__/handlers/graphql/logger.ts @@ -0,0 +1,115 @@ +import { ICreateMutationCb, ICreateQueryCb, MutationBody } from "../helpers/factory/types"; + +const ERROR_FIELDS = ` +error { + message + code + data + stack +} +`; + +const LOG_FIELDS = ` +data { + id + type + source + data + createdOn +} +`; + +/** + * Queries + */ +const createGetLogQuery = () => { + return ` + query GetLog($where: GetLogWhereInput!) { + logs { + getLog(where: $where) { + ${LOG_FIELDS} + ${ERROR_FIELDS} + } + } + } + `; +}; +const createListLogsQuery = () => { + return ` + query ListLogs( + $where: ListLogsWhereInput + $sort: ListLogsSortEnum + $limit: Int + $after: String + ) { + logs { + listLogs(where: $where, sort: $sort, limit: $limit, after: $after) { + ${LOG_FIELDS} + ${ERROR_FIELDS} + meta { + cursor + hasMoreItems + totalCount + } + } + } + } + `; +}; +/** + * Mutations + */ + +const createDeleteLogMutation = (): MutationBody => { + return `mutation DeleteLog($tenants: [String!]!, $item: String!) { + logs { + deleteLog(tenants: $tenants, item: $item) { + data + ${ERROR_FIELDS} + } + } + } + `; +}; + +const createDeleteLogsMutation = (): MutationBody => { + return `mutation DeleteLog($tenants: [String!]!, $items: [String!]!) { + logs { + deleteLogs(tenants: $tenants, items: $items) { + data + ${ERROR_FIELDS} + } + } + } + `; +}; + +export interface ICreateLoggerGraphQlParams { + createQuery: ICreateQueryCb; + createMutation: ICreateMutationCb; +} + +export const createLoggerGraphQL = (params: ICreateLoggerGraphQlParams) => { + const { createQuery, createMutation } = params; + + return { + /** + * Queries + */ + getLog: createQuery({ + query: createGetLogQuery() + }), + listLogs: createQuery({ + query: createListLogsQuery() + }), + /** + * Mutations + */ + deleteLog: createMutation({ + mutation: createDeleteLogMutation() + }), + deleteLogs: createMutation({ + mutation: createDeleteLogsMutation() + }) + }; +}; diff --git a/packages/api-serverless-cms/__tests__/handlers/helpers/core.ts b/packages/api-serverless-cms/__tests__/handlers/helpers/core.ts index 6cdcc75575f..3357793884a 100644 --- a/packages/api-serverless-cms/__tests__/handlers/helpers/core.ts +++ b/packages/api-serverless-cms/__tests__/handlers/helpers/core.ts @@ -66,6 +66,7 @@ export interface ICreateCoreResult { adminUsersStorage: AdminUsersStorageOperations; tenant: Pick; login: (identity?: SecurityIdentity | null) => void; + logout: () => void; } export const createCore = (params: ICreateCoreParams): ICreateCoreResult => { @@ -99,6 +100,7 @@ export const createCore = (params: ICreateCoreParams): ICreateCoreResult => { adminUsersStorage: adminUsersStorage.storageOperations, tenant: security.tenant, login: security.login, + logout: security.logout, plugins: [ enableBenchmarkOnEnvironmentVariable(), createWcpContext(), @@ -152,7 +154,8 @@ export const createCore = (params: ICreateCoreParams): ICreateCoreResult => { ...adminUsersStorage.plugins, ...security.plugins, createLogger({ - documentClient + documentClient, + createGraphQL: true }), createAdminUsersApp({ storageOperations: adminUsersStorage.storageOperations diff --git a/packages/api-serverless-cms/__tests__/handlers/helpers/tenancySecurity.ts b/packages/api-serverless-cms/__tests__/handlers/helpers/tenancySecurity.ts index 0c810a7c967..f5ff06342bc 100644 --- a/packages/api-serverless-cms/__tests__/handlers/helpers/tenancySecurity.ts +++ b/packages/api-serverless-cms/__tests__/handlers/helpers/tenancySecurity.ts @@ -97,6 +97,9 @@ export const createTenancyAndSecurity = ({ permissions, tenant }: IConfig) => { login.setIdentity( identity === null ? null : identity || getDefaultIdentity(permissions) ); + }, + logout: () => { + login.setIdentity(null); } }; }; diff --git a/packages/api-serverless-cms/__tests__/handlers/logger/logger.test.ts b/packages/api-serverless-cms/__tests__/handlers/logger/logger.test.ts new file mode 100644 index 00000000000..2a7116abbff --- /dev/null +++ b/packages/api-serverless-cms/__tests__/handlers/logger/logger.test.ts @@ -0,0 +1,103 @@ +import { useGraphQlHandler } from "~tests/handlers/graphQlHandler"; + +describe("logger graphql", () => { + const { getLog, listLogs, login, logout } = useGraphQlHandler({ + path: "/graphql", + features: true + }); + + beforeEach(async () => { + logout(); + process.env.S3_BUCKET = "a-mock-s3-bucket-which-does-not-exist"; + }); + + it("should list all logs", async () => { + const [notAuthorizedResult] = await listLogs(); + + expect(notAuthorizedResult).toMatchObject({ + data: { + logs: { + listLogs: { + data: null, + meta: null, + error: { + code: "SECURITY_NOT_AUTHORIZED", + data: null, + message: "Not authorized!" + } + } + } + } + }); + + login(); + + const [result] = await listLogs(); + + expect(result).toEqual({ + data: { + logs: { + listLogs: { + data: [], + meta: { + cursor: null, + hasMoreItems: false, + totalCount: -1 + }, + error: null + } + } + } + }); + }); + + it("should get a single log", async () => { + const [notAuthorizedResult] = await getLog({ + variables: { + where: { + id: "1" + } + } + }); + + expect(notAuthorizedResult).toMatchObject({ + data: { + logs: { + getLog: { + data: null, + error: { + code: "SECURITY_NOT_AUTHORIZED", + data: null, + message: "Not authorized!" + } + } + } + } + }); + + login(); + + const [result] = await getLog({ + variables: { + where: { + id: "1" + } + } + }); + + expect(result).toMatchObject({ + data: { + logs: { + getLog: { + data: null, + error: { + code: "NOT_FOUND", + data: null, + message: "Not found." + } + } + } + } + }); + }); +}); diff --git a/packages/api-serverless-cms/package.json b/packages/api-serverless-cms/package.json index b911294303b..88099deae96 100644 --- a/packages/api-serverless-cms/package.json +++ b/packages/api-serverless-cms/package.json @@ -16,6 +16,7 @@ "@webiny/api-headless-cms": "0.0.0", "@webiny/api-i18n": "0.0.0", "@webiny/api-i18n-content": "0.0.0", + "@webiny/api-log": "0.0.0", "@webiny/api-mailer": "0.0.0", "@webiny/api-page-builder": "0.0.0", "@webiny/api-page-builder-aco": "0.0.0", @@ -32,7 +33,6 @@ "@webiny/api-audit-logs": "0.0.0", "@webiny/api-headless-cms-aco": "0.0.0", "@webiny/api-headless-cms-tasks": "0.0.0", - "@webiny/api-log": "0.0.0", "@webiny/api-page-builder-import-export": "0.0.0", "@webiny/api-page-builder-import-export-so-ddb": "0.0.0", "@webiny/api-record-locking": "0.0.0", diff --git a/packages/api-serverless-cms/src/index.ts b/packages/api-serverless-cms/src/index.ts index 4d68356f449..5694ac96ab0 100644 --- a/packages/api-serverless-cms/src/index.ts +++ b/packages/api-serverless-cms/src/index.ts @@ -10,13 +10,14 @@ import { FormBuilderContext } from "@webiny/api-form-builder/types"; import { CmsContext } from "@webiny/api-headless-cms/types"; import { AcoContext } from "@webiny/api-aco/types"; import { PbAcoContext } from "@webiny/api-page-builder-aco/types"; -import { createContextPlugin as baseCreateContextPlugin, ContextPluginCallable } from "@webiny/api"; +import { ContextPluginCallable, createContextPlugin as baseCreateContextPlugin } from "@webiny/api"; import { createGraphQLSchemaPlugin as baseCreateGraphQLSchemaPlugin, GraphQLSchemaPluginConfig } from "@webiny/handler-graphql"; import { createSecurityRolePlugin, createSecurityTeamPlugin } from "@webiny/api-security"; import { MailerContext } from "@webiny/api-mailer/types"; +import { Context as LoggerContext } from "@webiny/api-log/types"; export interface Context extends ClientContext, @@ -31,6 +32,7 @@ export interface Context FormBuilderContext, AcoContext, PbAcoContext, + LoggerContext, CmsContext {} export const createContextPlugin = ( diff --git a/packages/api-serverless-cms/tsconfig.build.json b/packages/api-serverless-cms/tsconfig.build.json index 33078b23d71..fd307abbbbf 100644 --- a/packages/api-serverless-cms/tsconfig.build.json +++ b/packages/api-serverless-cms/tsconfig.build.json @@ -9,6 +9,7 @@ { "path": "../api-headless-cms/tsconfig.build.json" }, { "path": "../api-i18n/tsconfig.build.json" }, { "path": "../api-i18n-content/tsconfig.build.json" }, + { "path": "../api-log/tsconfig.build.json" }, { "path": "../api-mailer/tsconfig.build.json" }, { "path": "../api-page-builder/tsconfig.build.json" }, { "path": "../api-page-builder-aco/tsconfig.build.json" }, @@ -23,7 +24,6 @@ { "path": "../api-audit-logs/tsconfig.build.json" }, { "path": "../api-headless-cms-aco/tsconfig.build.json" }, { "path": "../api-headless-cms-tasks/tsconfig.build.json" }, - { "path": "../api-log/tsconfig.build.json" }, { "path": "../api-page-builder-import-export/tsconfig.build.json" }, { "path": "../api-page-builder-import-export-so-ddb/tsconfig.build.json" }, { "path": "../api-record-locking/tsconfig.build.json" }, diff --git a/packages/api-serverless-cms/tsconfig.json b/packages/api-serverless-cms/tsconfig.json index 62854fe63c3..0cf20781a0b 100644 --- a/packages/api-serverless-cms/tsconfig.json +++ b/packages/api-serverless-cms/tsconfig.json @@ -9,6 +9,7 @@ { "path": "../api-headless-cms" }, { "path": "../api-i18n" }, { "path": "../api-i18n-content" }, + { "path": "../api-log" }, { "path": "../api-mailer" }, { "path": "../api-page-builder" }, { "path": "../api-page-builder-aco" }, @@ -23,7 +24,6 @@ { "path": "../api-audit-logs" }, { "path": "../api-headless-cms-aco" }, { "path": "../api-headless-cms-tasks" }, - { "path": "../api-log" }, { "path": "../api-page-builder-import-export" }, { "path": "../api-page-builder-import-export-so-ddb" }, { "path": "../api-record-locking" }, @@ -55,6 +55,8 @@ "@webiny/api-i18n": ["../api-i18n/src"], "@webiny/api-i18n-content/*": ["../api-i18n-content/src/*"], "@webiny/api-i18n-content": ["../api-i18n-content/src"], + "@webiny/api-log/*": ["../api-log/src/*"], + "@webiny/api-log": ["../api-log/src"], "@webiny/api-mailer/*": ["../api-mailer/src/*"], "@webiny/api-mailer": ["../api-mailer/src"], "@webiny/api-page-builder/*": ["../api-page-builder/src/*"], @@ -83,8 +85,6 @@ "@webiny/api-headless-cms-aco": ["../api-headless-cms-aco/src"], "@webiny/api-headless-cms-tasks/*": ["../api-headless-cms-tasks/src/*"], "@webiny/api-headless-cms-tasks": ["../api-headless-cms-tasks/src"], - "@webiny/api-log/*": ["../api-log/src/*"], - "@webiny/api-log": ["../api-log/src"], "@webiny/api-page-builder-import-export/*": ["../api-page-builder-import-export/src/*"], "@webiny/api-page-builder-import-export": ["../api-page-builder-import-export/src"], "@webiny/api-page-builder-import-export-so-ddb/*": [ diff --git a/packages/pulumi-aws/src/apps/api/ApiApwScheduler.ts b/packages/pulumi-aws/src/apps/api/ApiApwScheduler.ts index 23e60bc0a54..00f664b025b 100644 --- a/packages/pulumi-aws/src/apps/api/ApiApwScheduler.ts +++ b/packages/pulumi-aws/src/apps/api/ApiApwScheduler.ts @@ -155,6 +155,20 @@ function createExecuteActionLambdaPolicy(app: PulumiApp) { pulumi.interpolate`${core.primaryDynamodbTableArn}`, pulumi.interpolate`${core.primaryDynamodbTableArn}/*` ] + }, + { + Sid: "PermissionDynamoDBLog", + Effect: "Allow", + Action: [ + "dynamodb:Query", + "dynamodb:GetItem", + "dynamodb:DeleteItem", + "dynamodb:PutItem" + ], + Resource: [ + pulumi.interpolate`${core.logDynamodbTableArn}`, + pulumi.interpolate`${core.logDynamodbTableArn}/*` + ] } ] } diff --git a/packages/pulumi-aws/src/apps/api/ApiFileManager.ts b/packages/pulumi-aws/src/apps/api/ApiFileManager.ts index c80a7b99350..0a88d1b697c 100644 --- a/packages/pulumi-aws/src/apps/api/ApiFileManager.ts +++ b/packages/pulumi-aws/src/apps/api/ApiFileManager.ts @@ -153,6 +153,20 @@ function createFileManagerLambdaPolicy(app: PulumiApp) { pulumi.interpolate`${core.primaryDynamodbTableArn}`, pulumi.interpolate`${core.primaryDynamodbTableArn}/*` ] + }, + { + Sid: "PermissionForDynamoDBLog", + Effect: "Allow", + Action: [ + "dynamodb:GetItem", + "dynamodb:Query", + "dynamodb:PutItem", + "dynamodb:BatchWriteItem" + ], + Resource: [ + pulumi.interpolate`${core.logDynamodbTableArn}`, + pulumi.interpolate`${core.logDynamodbTableArn}/*` + ] } ] } diff --git a/packages/pulumi-aws/src/apps/api/ApiGraphql.ts b/packages/pulumi-aws/src/apps/api/ApiGraphql.ts index 55726a6b95f..f33d16ef3a6 100644 --- a/packages/pulumi-aws/src/apps/api/ApiGraphql.ts +++ b/packages/pulumi-aws/src/apps/api/ApiGraphql.ts @@ -223,6 +223,8 @@ function createGraphqlLambdaPolicy(app: PulumiApp) { Resource: [ `${core.primaryDynamodbTableArn}`, `${core.primaryDynamodbTableArn}/*`, + `${core.logDynamodbTableArn}`, + `${core.logDynamodbTableArn}/*`, // Attach permissions for elastic search dynamo as well (if ES is enabled). ...(core.elasticsearchDynamodbTableArn ? [ diff --git a/packages/pulumi-aws/src/apps/api/ApiPageBuilder.ts b/packages/pulumi-aws/src/apps/api/ApiPageBuilder.ts index 4222b0dc934..983bd9cbcda 100644 --- a/packages/pulumi-aws/src/apps/api/ApiPageBuilder.ts +++ b/packages/pulumi-aws/src/apps/api/ApiPageBuilder.ts @@ -138,7 +138,9 @@ function createExportLambdaPolicy(app: PulumiApp) { ], Resource: [ pulumi.interpolate`${core.primaryDynamodbTableArn}`, - pulumi.interpolate`${core.primaryDynamodbTableArn}/*` + pulumi.interpolate`${core.primaryDynamodbTableArn}/*`, + pulumi.interpolate`${core.logDynamodbTableArn}`, + pulumi.interpolate`${core.logDynamodbTableArn}/*` ] }, { diff --git a/packages/pulumi-aws/src/apps/website/WebsitePrerendering.ts b/packages/pulumi-aws/src/apps/website/WebsitePrerendering.ts index 1804b42c2f0..447f0fe503c 100644 --- a/packages/pulumi-aws/src/apps/website/WebsitePrerendering.ts +++ b/packages/pulumi-aws/src/apps/website/WebsitePrerendering.ts @@ -327,7 +327,9 @@ function createLambdaPolicy( // Add permissions to DynamoDB table const resources = [ `${s.primaryDynamodbTableArn}`, - `${s.primaryDynamodbTableArn}/*` + `${s.primaryDynamodbTableArn}/*`, + `${s.logDynamodbTableArn}`, + `${s.logDynamodbTableArn}/*` ]; // Attach permissions for elastic search dynamo as well (if ES is enabled). diff --git a/yarn.lock b/yarn.lock index cc4a2e06032..b93786855cf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13030,7 +13030,9 @@ __metadata: "@webiny/api-tenancy": "npm:0.0.0" "@webiny/aws-sdk": "npm:0.0.0" "@webiny/cli": "npm:0.0.0" + "@webiny/db": "npm:0.0.0" "@webiny/db-dynamodb": "npm:0.0.0" + "@webiny/error": "npm:0.0.0" "@webiny/handler": "npm:0.0.0" "@webiny/handler-graphql": "npm:0.0.0" "@webiny/plugins": "npm:0.0.0"