Skip to content

Commit

Permalink
Add API endpoints for adding and removing block rules
Browse files Browse the repository at this point in the history
  • Loading branch information
forgetso committed Dec 23, 2024
1 parent 2dd7ee8 commit 05f156d
Show file tree
Hide file tree
Showing 19 changed files with 293 additions and 119 deletions.
35 changes: 21 additions & 14 deletions packages/cli/src/commands/addBlockRules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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: {
Expand All @@ -92,20 +95,24 @@ 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,
}),
);
}
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("IP Block rules added");
Expand Down
37 changes: 36 additions & 1 deletion packages/database/src/databases/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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<IPBlockRuleRecord, "ip">]: { $in: number[] };
} & {
[key in keyof Pick<IPBlockRuleRecord, "dappAccount">]?: 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
*/
Expand Down Expand Up @@ -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<UserAccountBlockRule, "userAccount">]: {
$in: string[];
};
} & {
[key in keyof Pick<UserAccountBlockRule, "dappAccount">]?: string; // Optional `dappAccount` key
} = { userAccount: { $in: userAccounts } };
if (dappAccount) {
filter.dappAccount = dappAccount;
}
await this.tables?.userblockrules.deleteMany(filter);
}
}
65 changes: 65 additions & 0 deletions packages/provider/src/api/admin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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;
}
7 changes: 4 additions & 3 deletions packages/provider/src/rules/ip.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { BlockRule, IPAddress } from "@prosopo/types";
// Copyright 2021-2024 Prosopo (UK) Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
Expand All @@ -11,12 +12,12 @@
// 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 { IProviderDatabase } from "@prosopo/types-database";

export const checkIpRules = async (
db: IProviderDatabase,
ipAddress: Address4 | Address6,
ipAddress: IPAddress,
dapp: string,
): Promise<BlockRule | undefined> => {
const rule = await db.getIPBlockRuleRecord(ipAddress.bigInt());
Expand Down
4 changes: 3 additions & 1 deletion packages/provider/src/rules/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
94 changes: 60 additions & 34 deletions packages/provider/src/tasks/client/clientTasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -218,48 +221,71 @@ export class ClientTaskManager {
]);
}

async addIPBlockRules(
ips: string[],
global: boolean,
hardBlock: boolean,
dappAccount?: string,
captchaConfig?: CaptchaConfig,
): Promise<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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;
Expand Down
6 changes: 3 additions & 3 deletions packages/provider/src/tasks/dataset/datasetTasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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;
Expand Down
Loading

0 comments on commit 05f156d

Please sign in to comment.