From cf2cf7f6753da96e060601e3abc36251cba79f30 Mon Sep 17 00:00:00 2001 From: Chris Date: Mon, 23 Dec 2024 12:46:43 +0000 Subject: [PATCH] Add API endpoints for adding and removing block rules (#1594) --- packages/cli/src/RateLimiter.ts | 16 ++++ packages/cli/src/commands/addBlockRules.ts | 38 +++++--- packages/database/src/databases/provider.ts | 37 +++++++- packages/provider/src/api/admin.ts | 65 +++++++++++++ packages/provider/src/rules/ip.ts | 7 +- packages/provider/src/rules/user.ts | 4 +- .../provider/src/tasks/client/clientTasks.ts | 94 ++++++++++++------- .../src/tasks/dataset/datasetTasks.ts | 6 +- .../tasks/frictionless/frictionlessTasks.ts | 6 +- .../src/tasks/imgCaptcha/imgCaptchaTasks.ts | 6 +- .../tasks/imgCaptcha/imgCaptchaTasksUtils.ts | 8 +- .../provider/src/tasks/powCaptcha/powTasks.ts | 4 +- packages/provider/src/tasks/tasks.ts | 7 +- .../tasks/dataset/datasetTasks.unit.test.ts | 6 +- .../imgCaptchaTasksUtils.unit.test.ts | 12 +-- packages/provider/src/util.ts | 8 +- packages/types-database/src/types/provider.ts | 24 ++--- packages/types/src/config/config.ts | 22 +---- packages/types/src/datasets/captcha.ts | 15 --- packages/types/src/provider/api.ts | 86 +++++++++++++++-- 20 files changed, 331 insertions(+), 140 deletions(-) diff --git a/packages/cli/src/RateLimiter.ts b/packages/cli/src/RateLimiter.ts index 823fcbf8f9..600d0384a1 100644 --- a/packages/cli/src/RateLimiter.ts +++ b/packages/cli/src/RateLimiter.ts @@ -59,5 +59,21 @@ export const getRateLimitConfig = () => { windowMs: process.env.PROSOPO_GET_FR_CAPTCHA_CHALLENGE_WINDOW, limit: process.env.PROSOPO_GET_FR_CAPTCHA_CHALLENGE_LIMIT, }, + [AdminApiPaths.BlockRuleIPAdd]: { + windowMs: process.env.PROSOPO_BLOCK_RULE_IP_ADD_WINDOW, + limit: process.env.PROSOPO_BLOCK_RULE_IP_ADD_LIMIT, + }, + [AdminApiPaths.BlockRuleIPRemove]: { + windowMs: process.env.PROSOPO_BLOCK_RULE_IP_REMOVE_WINDOW, + limit: process.env.PROSOPO_BLOCK_RULE_IP_REMOVE_LIMIT, + }, + [AdminApiPaths.BlocKRuleUserAdd]: { + windowMs: process.env.PROSOPO_BLOCK_RULE_USER_ADD_WINDOW, + limit: process.env.PROSOPO_BLOCK_RULE_USER_ADD_LIMIT, + }, + [AdminApiPaths.BlockRuleUserRemove]: { + windowMs: process.env.PROSOPO_BLOCK_RULE_USER_REMOVE_WINDOW, + limit: process.env.PROSOPO_BLOCK_RULE_USER_REMOVE_LIMIT, + }, }; }; diff --git a/packages/cli/src/commands/addBlockRules.ts b/packages/cli/src/commands/addBlockRules.ts index 0921c3811c..fc917180d8 100644 --- a/packages/cli/src/commands/addBlockRules.ts +++ b/packages/cli/src/commands/addBlockRules.ts @@ -16,10 +16,13 @@ import type { KeyringPair } from "@polkadot/keyring/types"; import { LogLevel, type Logger, getLogger } from "@prosopo/common"; import { ProviderEnvironment } from "@prosopo/env"; import { Tasks } from "@prosopo/provider"; -import type { CaptchaConfig, ProsopoConfigOutput } from "@prosopo/types"; +import { + AddBlockRulesIPSpec, + AddBlockRulesUserSpec, + type ProsopoCaptchaCountConfigSchemaOutput, + type ProsopoConfigOutput, +} from "@prosopo/types"; import type { ArgumentsCamelCase, Argv } from "yargs"; -import * as z from "zod"; -import { loadJSONFile } from "../files.js"; export default ( pair: KeyringPair, @@ -78,7 +81,7 @@ export default ( const env = new ProviderEnvironment(config, pair); await env.isReady(); const tasks = new Tasks(env); - let captchaConfig: CaptchaConfig | undefined; + let captchaConfig: ProsopoCaptchaCountConfigSchemaOutput | undefined; if (argv.solved) { captchaConfig = { solved: { @@ -92,23 +95,28 @@ export default ( if (argv.ips) { await tasks.clientTaskManager.addIPBlockRules( - argv.ips as unknown as string[], - argv.global as boolean, - argv.hardBlock as boolean, - argv.dapp as unknown as string, - captchaConfig, + AddBlockRulesIPSpec.parse({ + ips: argv.ips, + global: argv.global, + hardBlock: argv.hardBlock, + dapp: argv.dapp, + captchaConfig, + }), ); + logger.info("IP Block rules added"); } if (argv.users) { await tasks.clientTaskManager.addUserBlockRules( - argv.users as unknown as string[], - argv.hardBlock as boolean, - argv.global as boolean, - argv.dapp as unknown as string, - captchaConfig, + AddBlockRulesUserSpec.parse({ + users: argv.users, + global: argv.global, + hardBlock: argv.hardBlock, + dapp: argv.dapp, + captchaConfig, + }), ); + logger.info("User Block rules added"); } - logger.info("IP Block rules added"); } catch (err) { logger.error(err); } diff --git a/packages/database/src/databases/provider.ts b/packages/database/src/databases/provider.ts index 2ddb8a6be8..b0e7039a8c 100644 --- a/packages/database/src/databases/provider.ts +++ b/packages/database/src/databases/provider.ts @@ -1455,7 +1455,7 @@ export class ProviderDatabase } /** - * @description Check if a request has a blocking rule associated with it + * @description Store IP blocking rule records */ async storeIPBlockRuleRecords(rules: IPBlockRuleRecord[]) { await this.tables?.ipblockrules.bulkWrite( @@ -1469,6 +1469,21 @@ export class ProviderDatabase ); } + /** + * @description Remove IP blocking rule records + */ + async removeIPBlockRuleRecords(ipAddresses: bigint[], dappAccount?: string) { + const filter: { + [key in keyof Pick]: { $in: number[] }; + } & { + [key in keyof Pick]?: string; // Optional `dappAccount` key + } = { ip: { $in: ipAddresses.map(Number) } }; + if (dappAccount) { + filter.dappAccount = dappAccount; + } + await this.tables?.ipblockrules.deleteMany(filter); + } + /** * @description Check if a request has a blocking rule associated with it */ @@ -1503,4 +1518,24 @@ export class ProviderDatabase })), ); } + + /** + * @description Remove user blocking rule records + */ + async removeUserBlockRuleRecords( + userAccounts: string[], + dappAccount?: string, + ) { + const filter: { + [key in keyof Pick]: { + $in: string[]; + }; + } & { + [key in keyof Pick]?: string; // Optional `dappAccount` key + } = { userAccount: { $in: userAccounts } }; + if (dappAccount) { + filter.dappAccount = dappAccount; + } + await this.tables?.userblockrules.deleteMany(filter); + } } diff --git a/packages/provider/src/api/admin.ts b/packages/provider/src/api/admin.ts index 74edb2891d..a02e8bdef9 100644 --- a/packages/provider/src/api/admin.ts +++ b/packages/provider/src/api/admin.ts @@ -13,9 +13,14 @@ import { Logger, logError } from "@prosopo/common"; // See the License for the specific language governing permissions and // limitations under the License. import { + AddBlockRulesIPSpec, + AddBlockRulesUserSpec, AdminApiPaths, type ApiResponse, + BlockRuleIPAddBody, RegisterSitekeyBody, + RemoveBlockRulesIPSpec, + RemoveBlockRulesUserSpec, } from "@prosopo/types"; import type { ProviderEnvironment } from "@prosopo/types-env"; import { Router } from "express"; @@ -42,5 +47,65 @@ export function prosopoAdminRouter(env: ProviderEnvironment): Router { } }); + router.post(AdminApiPaths.BlockRuleIPAdd, async (req, res, next) => { + try { + tasks.logger.info("Adding block rules"); + const parsed = AddBlockRulesIPSpec.parse(req.body); + await tasks.clientTaskManager.addIPBlockRules(parsed); + const response: ApiResponse = { + status: "success", + }; + res.json(response); + } catch (err) { + logError(err, tasks.logger); + res.status(400).send("An internal server error occurred."); + } + }); + + router.post(AdminApiPaths.BlockRuleIPRemove, async (req, res, next) => { + try { + tasks.logger.info("Removing block rules"); + const parsed = RemoveBlockRulesIPSpec.parse(req.body); + await tasks.clientTaskManager.removeIPBlockRules(parsed); + const response: ApiResponse = { + status: "success", + }; + res.json(response); + } catch (err) { + logError(err, tasks.logger); + res.status(400).send("An internal server error occurred."); + } + }); + + router.post(AdminApiPaths.BlocKRuleUserAdd, async (req, res, next) => { + try { + tasks.logger.info("Adding block rules"); + const parsed = AddBlockRulesUserSpec.parse(req.body); + await tasks.clientTaskManager.addUserBlockRules(parsed); + const response: ApiResponse = { + status: "success", + }; + res.json(response); + } catch (err) { + logError(err, tasks.logger); + res.status(400).send("An internal server error occurred."); + } + }); + + router.post(AdminApiPaths.BlockRuleUserRemove, async (req, res, next) => { + try { + tasks.logger.info("Removing block rules"); + const parsed = RemoveBlockRulesUserSpec.parse(req.body); + await tasks.clientTaskManager.removeUserBlockRules(parsed); + const response: ApiResponse = { + status: "success", + }; + res.json(response); + } catch (err) { + logError(err, tasks.logger); + res.status(400).send("An internal server error occurred."); + } + }); + return router; } diff --git a/packages/provider/src/rules/ip.ts b/packages/provider/src/rules/ip.ts index 986a2d0b5a..23ddcdc09c 100644 --- a/packages/provider/src/rules/ip.ts +++ b/packages/provider/src/rules/ip.ts @@ -11,12 +11,13 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -import type { BlockRule, IProviderDatabase } from "@prosopo/types-database"; -import type { Address4, Address6 } from "ip-address"; + +import type { BlockRule, IPAddress } from "@prosopo/types"; +import type { IProviderDatabase } from "@prosopo/types-database"; export const checkIpRules = async ( db: IProviderDatabase, - ipAddress: Address4 | Address6, + ipAddress: IPAddress, dapp: string, ): Promise => { const rule = await db.getIPBlockRuleRecord(ipAddress.bigInt()); diff --git a/packages/provider/src/rules/user.ts b/packages/provider/src/rules/user.ts index 790b5c3699..698009f910 100644 --- a/packages/provider/src/rules/user.ts +++ b/packages/provider/src/rules/user.ts @@ -11,7 +11,9 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -import type { BlockRule, IProviderDatabase } from "@prosopo/types-database"; + +import type { BlockRule } from "@prosopo/types"; +import type { IProviderDatabase } from "@prosopo/types-database"; export const checkUserRules = async ( db: IProviderDatabase, diff --git a/packages/provider/src/tasks/client/clientTasks.ts b/packages/provider/src/tasks/client/clientTasks.ts index 63b9ac0af5..84fcba5c21 100644 --- a/packages/provider/src/tasks/client/clientTasks.ts +++ b/packages/provider/src/tasks/client/clientTasks.ts @@ -16,20 +16,23 @@ import { validateAddress } from "@polkadot/util-crypto/address"; import { type Logger, ProsopoApiError } from "@prosopo/common"; import { CaptchaDatabase, ClientDatabase } from "@prosopo/database"; import { - type CaptchaConfig, + type AddBlockRulesIP, + type AddBlockRulesUser, + BlockRuleType, type IUserSettings, type ProsopoConfigOutput, + type RemoveBlockRulesIP, + type RemoveBlockRulesUser, ScheduledTaskNames, ScheduledTaskStatus, } from "@prosopo/types"; -import { - BlockRuleType, - type ClientRecord, - type IPAddressBlockRule, - type IProviderDatabase, - type PoWCaptchaStored, - type UserAccountBlockRule, - type UserCommitment, +import type { + ClientRecord, + IPAddressBlockRule, + IProviderDatabase, + PoWCaptchaStored, + UserAccountBlockRule, + UserCommitment, } from "@prosopo/types-database"; import { parseUrl } from "@prosopo/util"; import { getIPAddress } from "../../util.js"; @@ -218,48 +221,71 @@ export class ClientTaskManager { ]); } - async addIPBlockRules( - ips: string[], - global: boolean, - hardBlock: boolean, - dappAccount?: string, - captchaConfig?: CaptchaConfig, - ): Promise { - const rules: IPAddressBlockRule[] = ips.map((ip) => { + /** + * @description Add IP block rules to the database. Allows specifying mutiple IPs for a single configuration + * @param {AddBlockRulesIP} opts + */ + async addIPBlockRules(opts: AddBlockRulesIP): Promise { + const rules: IPAddressBlockRule[] = opts.ips.map((ip) => { return { ip: Number(getIPAddress(ip).bigInt()), - global, + global: opts.global, type: BlockRuleType.ipAddress, - dappAccount, - hardBlock, - ...(captchaConfig && { captchaConfig }), + dappAccount: opts.dappAccount, + hardBlock: opts.hardBlock, + ...(opts.captchaConfig && { captchaConfig: opts.captchaConfig }), }; }); await this.providerDB.storeIPBlockRuleRecords(rules); } - async addUserBlockRules( - userAccounts: string[], - hardBlock: boolean, - global: boolean, - dappAccount?: string, - captchaConfig?: CaptchaConfig, - ): Promise { - validateAddress(dappAccount, false, 42); - const rules: UserAccountBlockRule[] = userAccounts.map((userAccount) => { + /** + * @description Remove IP block rules from the database by IP address and optionally dapp account + * @param {RemoveBlockRulesIP} opts + */ + async removeIPBlockRules(opts: RemoveBlockRulesIP): Promise { + await this.providerDB.removeIPBlockRuleRecords( + opts.ips.map((ip) => getIPAddress(ip).bigInt()), + opts.dappAccount, + ); + } + + /** + * @description Add user block rules to the database. Allows specifying multiple users for a single configuration + * @param {AddBlockRulesUser} opts + */ + async addUserBlockRules(opts: AddBlockRulesUser): Promise { + validateAddress(opts.dappAccount, false, 42); + const rules: UserAccountBlockRule[] = opts.users.map((userAccount) => { validateAddress(userAccount, false, 42); return { - dappAccount, + dappAccount: opts.dappAccount, userAccount, type: BlockRuleType.userAccount, - global, - hardBlock, - ...(captchaConfig && { captchaConfig }), + global: opts.global, + hardBlock: opts.hardBlock, + ...(opts.captchaConfig && { captchaConfig: opts.captchaConfig }), }; }); await this.providerDB.storeUserBlockRuleRecords(rules); } + /** + * @description Remove user block rules from the database by user account and optionally dapp account + * @param {RemoveBlockRulesUser} opts + */ + async removeUserBlockRules(opts: RemoveBlockRulesUser): Promise { + if (opts.dappAccount) { + validateAddress(opts.dappAccount, false, 42); + await this.providerDB.removeUserBlockRuleRecords( + opts.users, + opts.dappAccount, + ); + } else { + await this.providerDB.removeUserBlockRuleRecords(opts.users); + } + } + isSubdomainOrExactMatch(referrer: string, clientDomain: string): boolean { if (!referrer || !clientDomain) return false; if (clientDomain === "*") return true; diff --git a/packages/provider/src/tasks/dataset/datasetTasks.ts b/packages/provider/src/tasks/dataset/datasetTasks.ts index b484c1c06c..d15403f12a 100644 --- a/packages/provider/src/tasks/dataset/datasetTasks.ts +++ b/packages/provider/src/tasks/dataset/datasetTasks.ts @@ -14,8 +14,8 @@ import type { Logger } from "@prosopo/common"; // limitations under the License. import { parseCaptchaDataset } from "@prosopo/datasets"; import type { - CaptchaConfig, DatasetRaw, + ProsopoCaptchaCountConfigSchemaOutput, ProsopoConfigOutput, } from "@prosopo/types"; import type { IProviderDatabase } from "@prosopo/types-database"; @@ -24,13 +24,13 @@ import { providerValidateDataset } from "./datasetTasksUtils.js"; export class DatasetManager { config: ProsopoConfigOutput; logger: Logger; - captchaConfig: CaptchaConfig; + captchaConfig: ProsopoCaptchaCountConfigSchemaOutput; db: IProviderDatabase; constructor( config: ProsopoConfigOutput, logger: Logger, - captchaConfig: CaptchaConfig, + captchaConfig: ProsopoCaptchaCountConfigSchemaOutput, db: IProviderDatabase, ) { this.config = config; diff --git a/packages/provider/src/tasks/frictionless/frictionlessTasks.ts b/packages/provider/src/tasks/frictionless/frictionlessTasks.ts index 47efdd78f5..c50157e4f9 100644 --- a/packages/provider/src/tasks/frictionless/frictionlessTasks.ts +++ b/packages/provider/src/tasks/frictionless/frictionlessTasks.ts @@ -19,6 +19,7 @@ import { type CaptchaResult, CaptchaStatus, type GetFrictionlessCaptchaResponse, + type IPAddress, POW_SEPARATOR, type PoWCaptcha, type PoWChallengeId, @@ -49,10 +50,7 @@ export class FrictionlessManager { this.db = db; } - async checkIpRules( - ipAddress: Address4 | Address6, - dapp: string, - ): Promise { + async checkIpRules(ipAddress: IPAddress, dapp: string): Promise { const rule = await checkIpRules(this.db, ipAddress, dapp); return !!rule; } diff --git a/packages/provider/src/tasks/imgCaptcha/imgCaptchaTasks.ts b/packages/provider/src/tasks/imgCaptcha/imgCaptchaTasks.ts index 0e055ca51c..d68e352446 100644 --- a/packages/provider/src/tasks/imgCaptcha/imgCaptchaTasks.ts +++ b/packages/provider/src/tasks/imgCaptcha/imgCaptchaTasks.ts @@ -23,12 +23,12 @@ import { } from "@prosopo/datasets"; import { type Captcha, - type CaptchaConfig, type CaptchaSolution, CaptchaStatus, DEFAULT_IMAGE_CAPTCHA_TIMEOUT, type DappUserSolutionResult, type Hash, + type IPAddress, type ImageVerificationResponse, type PendingCaptchaRequest, type ProsopoCaptchaCountConfigSchemaOutput, @@ -88,9 +88,9 @@ export class ImgCaptchaManager { async getRandomCaptchasAndRequestHash( datasetId: Hash, userAccount: string, - ipAddress: Address4 | Address6, + ipAddress: IPAddress, headers: RequestHeaders, - captchaConfig: CaptchaConfig, + captchaConfig: ProsopoCaptchaCountConfigSchemaOutput, ): Promise<{ captchas: Captcha[]; requestHash: string; diff --git a/packages/provider/src/tasks/imgCaptcha/imgCaptchaTasksUtils.ts b/packages/provider/src/tasks/imgCaptcha/imgCaptchaTasksUtils.ts index 555a759b7a..7d2ee72436 100644 --- a/packages/provider/src/tasks/imgCaptcha/imgCaptchaTasksUtils.ts +++ b/packages/provider/src/tasks/imgCaptcha/imgCaptchaTasksUtils.ts @@ -17,12 +17,12 @@ import { computeCaptchaSolutionHash, } from "@prosopo/datasets"; import type { - CaptchaConfig, CaptchaSolution, + IPAddress, + ProsopoCaptchaCountConfigSchemaOutput, ProsopoConfigOutput, } from "@prosopo/types"; import type { IProviderDatabase } from "@prosopo/types-database"; -import type { Address4, Address6 } from "ip-address"; import { checkIpRules } from "../../rules/ip.js"; import { checkUserRules } from "../../rules/user.js"; @@ -67,10 +67,10 @@ export const buildTreeAndGetCommitmentId = ( export const getCaptchaConfig = async ( db: IProviderDatabase, config: ProsopoConfigOutput, - ipAddress: Address4 | Address6, + ipAddress: IPAddress, user: string, dapp: string, -): Promise => { +): Promise => { const ipRule = await checkIpRules(db, ipAddress, dapp); if (ipRule) { return { diff --git a/packages/provider/src/tasks/powCaptcha/powTasks.ts b/packages/provider/src/tasks/powCaptcha/powTasks.ts index 0e1edb424c..c5a8bddea4 100644 --- a/packages/provider/src/tasks/powCaptcha/powTasks.ts +++ b/packages/provider/src/tasks/powCaptcha/powTasks.ts @@ -22,6 +22,7 @@ import { ApiParams, type CaptchaResult, CaptchaStatus, + type IPAddress, POW_SEPARATOR, type PoWCaptcha, type PoWChallengeId, @@ -29,7 +30,6 @@ import { } from "@prosopo/types"; import type { IProviderDatabase } from "@prosopo/types-database"; import { at, verifyRecency } from "@prosopo/util"; -import type { Address4, Address6 } from "ip-address"; import { checkPowSignature, validateSolution } from "./powTasksUtils.js"; const logger = getLoggerDefault(); @@ -95,7 +95,7 @@ export class PowCaptchaManager { nonce: number, timeout: number, userTimestampSignature: string, - ipAddress: Address4 | Address6, + ipAddress: IPAddress, headers: RequestHeaders, ): Promise { // Check signatures before doing DB reads to avoid unnecessary network connections diff --git a/packages/provider/src/tasks/tasks.ts b/packages/provider/src/tasks/tasks.ts index 41c7b29603..124669a705 100644 --- a/packages/provider/src/tasks/tasks.ts +++ b/packages/provider/src/tasks/tasks.ts @@ -13,7 +13,10 @@ import { type Logger, ProsopoEnvError, getLogger } from "@prosopo/common"; // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -import type { CaptchaConfig, ProsopoConfigOutput } from "@prosopo/types"; +import type { + ProsopoCaptchaCountConfigSchemaOutput, + ProsopoConfigOutput, +} from "@prosopo/types"; import type { IProviderDatabase } from "@prosopo/types-database"; import type { ProviderEnvironment } from "@prosopo/types-env"; import { ClientTaskManager } from "./client/clientTasks.js"; @@ -27,7 +30,7 @@ import { PowCaptchaManager } from "./powCaptcha/powTasks.js"; */ export class Tasks { db: IProviderDatabase; - captchaConfig: CaptchaConfig; + captchaConfig: ProsopoCaptchaCountConfigSchemaOutput; logger: Logger; config: ProsopoConfigOutput; pair: KeyringPair; diff --git a/packages/provider/src/tests/unit/tasks/dataset/datasetTasks.unit.test.ts b/packages/provider/src/tests/unit/tasks/dataset/datasetTasks.unit.test.ts index 7b4637c755..2d3a089b4a 100644 --- a/packages/provider/src/tests/unit/tasks/dataset/datasetTasks.unit.test.ts +++ b/packages/provider/src/tests/unit/tasks/dataset/datasetTasks.unit.test.ts @@ -15,8 +15,8 @@ import type { Logger } from "@prosopo/common"; import { parseCaptchaDataset } from "@prosopo/datasets"; import type { - CaptchaConfig, DatasetRaw, + ProsopoCaptchaCountConfigSchemaOutput, ProsopoConfigOutput, ScheduledTaskNames, ScheduledTaskResult, @@ -52,7 +52,7 @@ type TestScheduledTaskRecord = Pick< describe("DatasetManager", () => { let config: ProsopoConfigOutput; let logger: Logger; - let captchaConfig: CaptchaConfig; + let captchaConfig: ProsopoCaptchaCountConfigSchemaOutput; let providerDB: IProviderDatabase; let datasetManager: DatasetManager; // biome-ignore lint/suspicious/noExplicitAny: @@ -74,7 +74,7 @@ describe("DatasetManager", () => { captchaConfig = { solved: { count: 5 }, unsolved: { count: 5 }, - } as CaptchaConfig; + } as ProsopoCaptchaCountConfigSchemaOutput; collections.schedulers = {} as { records: Record; diff --git a/packages/provider/src/tests/unit/tasks/imgCaptcha/imgCaptchaTasksUtils.unit.test.ts b/packages/provider/src/tests/unit/tasks/imgCaptcha/imgCaptchaTasksUtils.unit.test.ts index 6a0fb5eae3..68a2a7cc36 100644 --- a/packages/provider/src/tests/unit/tasks/imgCaptcha/imgCaptchaTasksUtils.unit.test.ts +++ b/packages/provider/src/tests/unit/tasks/imgCaptcha/imgCaptchaTasksUtils.unit.test.ts @@ -16,13 +16,11 @@ import { CaptchaMerkleTree, computeCaptchaSolutionHash, } from "@prosopo/datasets"; -import type { CaptchaSolution } from "@prosopo/types"; -import { - BlockRuleType, - type IPAddressBlockRule, - type IProviderDatabase, - type UserAccountBlockRule, - type UserAccountBlockRuleRecord, +import { BlockRuleType, type CaptchaSolution } from "@prosopo/types"; +import type { + IPAddressBlockRule, + IProviderDatabase, + UserAccountBlockRule, } from "@prosopo/types-database"; import { Address4 } from "ip-address"; import { beforeEach, describe, expect, it, vi } from "vitest"; diff --git a/packages/provider/src/util.ts b/packages/provider/src/util.ts index 5fa290c6ca..6c9a5117f7 100644 --- a/packages/provider/src/util.ts +++ b/packages/provider/src/util.ts @@ -15,7 +15,11 @@ import { decodeAddress, encodeAddress } from "@polkadot/util-crypto/address"; import { hexToU8a } from "@polkadot/util/hex"; import { isHex } from "@polkadot/util/is"; import { ProsopoContractError, ProsopoEnvError } from "@prosopo/common"; -import { type ScheduledTaskNames, ScheduledTaskStatus } from "@prosopo/types"; +import { + type IPAddress, + type ScheduledTaskNames, + ScheduledTaskStatus, +} from "@prosopo/types"; import type { IDatabase, IProviderDatabase } from "@prosopo/types-database"; import { at } from "@prosopo/util"; import { Address4, Address6 } from "ip-address"; @@ -67,7 +71,7 @@ export async function checkIfTaskIsRunning( return false; } -export const getIPAddress = (ipAddressString: string): Address4 | Address6 => { +export const getIPAddress = (ipAddressString: string): IPAddress => { try { if (ipAddressString.match(/^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$/)) { return new Address4(ipAddressString); diff --git a/packages/types-database/src/types/provider.ts b/packages/types-database/src/types/provider.ts index 0a9337aaa0..4b506c346b 100644 --- a/packages/types-database/src/types/provider.ts +++ b/packages/types-database/src/types/provider.ts @@ -14,8 +14,9 @@ import { type TranslationKey, TranslationKeysSchema } from "@prosopo/locale"; import { + type BlockRule, + BlockRuleType, type Captcha, - type CaptchaConfig, type CaptchaResult, type CaptchaSolution, CaptchaSolutionSchema, @@ -32,6 +33,7 @@ import { type PoWCaptchaUser, type PoWChallengeComponents, type PoWChallengeId, + ProsopoCaptchaCountConfigSchema, type RequestHeaders, ScheduledTaskNames, type ScheduledTaskResult, @@ -39,7 +41,6 @@ import { type Timestamp, TimestampSchema, } from "@prosopo/types"; -import type { DeleteResult } from "mongodb"; import mongoose from "mongoose"; import { type Document, type Model, type ObjectId, Schema } from "mongoose"; import { @@ -384,18 +385,6 @@ export const SessionRecordSchema = new Schema({ SessionRecordSchema.index({ sessionId: 1 }, { unique: true }); -export type BlockRule = { - global: boolean; - type: BlockRuleType; - hardBlock: boolean; - captchaConfig?: CaptchaConfig; -}; - -export enum BlockRuleType { - ipAddress = "ipAddress", - userAccount = "userAccount", -} - export interface IPAddressBlockRule extends BlockRule { ip: number; dappAccount?: string; @@ -622,10 +611,17 @@ export interface IProviderDatabase extends IDatabase { storeIPBlockRuleRecords(rules: IPAddressBlockRule[]): Promise; + removeIPBlockRuleRecords(ips: bigint[], dappAccount?: string): Promise; + getUserBlockRuleRecord( userAccount: string, dappAccount: string, ): Promise; storeUserBlockRuleRecords(rules: UserAccountBlockRule[]): Promise; + + removeUserBlockRuleRecords( + users: string[], + dappAccount?: string, + ): Promise; } diff --git a/packages/types/src/config/config.ts b/packages/types/src/config/config.ts index fdba1bae5f..a0b22a090e 100644 --- a/packages/types/src/config/config.ts +++ b/packages/types/src/config/config.ts @@ -24,6 +24,7 @@ import type { infer as zInfer } from "zod"; import z, { boolean } from "zod"; import { ApiPathRateLimits, + ProsopoCaptchaCountConfigSchema, ProviderDefaultRateLimits, } from "../provider/index.js"; import { @@ -115,27 +116,6 @@ export const ProsopoBasicConfigSchema = ProsopoBaseConfigSchema.merge( export type ProsopoBasicConfigInput = input; export type ProsopoBasicConfigOutput = output; -export const ProsopoCaptchaCountConfigSchema = object({ - solved: object({ - count: number().positive(), - }) - .optional() - .default({ count: 1 }), - unsolved: object({ - count: number().nonnegative(), - }) - .optional() - .default({ count: 1 }), -}); - -export type ProsopoCaptchaCountConfigSchemaInput = input< - typeof ProsopoCaptchaCountConfigSchema ->; - -export type ProsopoCaptchaCountConfigSchemaOutput = output< - typeof ProsopoCaptchaCountConfigSchema ->; - export const ProsopoImageServerConfigSchema = object({ baseURL: string().url(), port: number().optional().default(9229), diff --git a/packages/types/src/datasets/captcha.ts b/packages/types/src/datasets/captcha.ts index deec4a478a..425f18a67e 100644 --- a/packages/types/src/datasets/captcha.ts +++ b/packages/types/src/datasets/captcha.ts @@ -160,21 +160,6 @@ export interface PoWCaptchaUser extends PoWCaptcha { dappAccount: DappAccount; } -export type CaptchaConfig = { - solved: { - count: number; - }; - unsolved: { - count: number; - }; -}; - -export type CaptchaSolutionConfig = { - requiredNumberOfSolutions: number; - solutionWinningPercentage: number; - captchaBlockRecency: number; -}; - export const CaptchaSchema = object({ captchaId: union([string(), zUndefined()]), captchaContentId: union([string(), zUndefined()]), diff --git a/packages/types/src/provider/api.ts b/packages/types/src/provider/api.ts index d72330facb..92611fa449 100644 --- a/packages/types/src/provider/api.ts +++ b/packages/types/src/provider/api.ts @@ -20,8 +20,10 @@ import { type ZodObject, type ZodOptional, array, + boolean, coerce, type input, + nativeEnum, number, object, type output, @@ -74,14 +76,12 @@ export type TGetImageCaptchaChallengePathAndParams = export type TGetImageCaptchaChallengeURL = `${string}${TGetImageCaptchaChallengePathAndParams}`; -export type TGetPowCaptchaChallengeURL = - `${string}${ApiPaths.GetPowCaptchaChallenge}`; - -export type TSubmitPowCaptchaSolutionURL = - `${string}${ApiPaths.SubmitPowCaptchaSolution}`; - export enum AdminApiPaths { SiteKeyRegister = "/v1/prosopo/provider/admin/sitekey/register", + BlockRuleIPAdd = "/v1/prosopo/provider/admin/blockrule/ip/add", + BlockRuleIPRemove = "/v1/prosopo/provider/admin/blockrule/ip/remove", + BlocKRuleUserAdd = "/v1/prosopo/provider/admin/blockrule/user/add", + BlockRuleUserRemove = "/v1/prosopo/provider/admin/blockrule/user/remove", } export type CombinedApiPaths = ApiPaths | AdminApiPaths; @@ -98,6 +98,10 @@ export const ProviderDefaultRateLimits = { [ApiPaths.GetProviderDetails]: { windowMs: 60000, limit: 60 }, [ApiPaths.SubmitUserEvents]: { windowMs: 60000, limit: 60 }, [AdminApiPaths.SiteKeyRegister]: { windowMs: 60000, limit: 5 }, + [AdminApiPaths.BlockRuleIPAdd]: { windowMs: 60000, limit: 5 }, + [AdminApiPaths.BlockRuleIPRemove]: { windowMs: 60000, limit: 5 }, + [AdminApiPaths.BlocKRuleUserAdd]: { windowMs: 60000, limit: 5 }, + [AdminApiPaths.BlockRuleUserRemove]: { windowMs: 60000, limit: 5 }, }; type RateLimit = { @@ -345,6 +349,76 @@ export const RegisterSitekeyBody = object({ }).optional(), }); +export const ProsopoCaptchaCountConfigSchema = object({ + solved: object({ + count: number().positive(), + }) + .optional() + .default({ count: 1 }), + unsolved: object({ + count: number().nonnegative(), + }) + .optional() + .default({ count: 0 }), +}); + +export type ProsopoCaptchaCountConfigSchemaInput = input< + typeof ProsopoCaptchaCountConfigSchema +>; + +export type ProsopoCaptchaCountConfigSchemaOutput = output< + typeof ProsopoCaptchaCountConfigSchema +>; + +export enum BlockRuleType { + ipAddress = "ipAddress", + userAccount = "userAccount", +} + +const BlockRuleTypeSpec = nativeEnum(BlockRuleType); + +export const BlockRuleSpec = object({ + global: boolean(), + hardBlock: boolean(), + type: BlockRuleTypeSpec, + dappAccount: string().optional(), + captchaConfig: ProsopoCaptchaCountConfigSchema.optional(), +}); + +export type BlockRule = zInfer; + +export const AddBlockRulesIPSpec = BlockRuleSpec.merge( + object({ + ips: array(string()), + }), +); + +export type AddBlockRulesIP = zInfer; + +export const RemoveBlockRulesIPSpec = object({ + ips: array(string()), + dappAccount: string().optional(), +}); + +export type RemoveBlockRulesIP = zInfer; + +export const BlockRuleIPAddBody = array(AddBlockRulesIPSpec); + +export const AddBlockRulesUserSpec = BlockRuleSpec.merge( + object({ + users: array(string()), + }), +); + +export type AddBlockRulesUser = zInfer; + +export const RemoveBlockRulesUserSpec = object({ + users: array(string()), + dappAccount: string().optional(), +}); + +export type RemoveBlockRulesUser = zInfer; + export const DappDomainRequestBody = object({ [ApiParams.dapp]: string(), });