Skip to content

Commit

Permalink
feature userAccessPolicy: grouping
Browse files Browse the repository at this point in the history
  • Loading branch information
light-source committed Jan 6, 2025
1 parent bb69dd1 commit 6f4af7f
Show file tree
Hide file tree
Showing 32 changed files with 1,275 additions and 19 deletions.
575 changes: 572 additions & 3 deletions package-lock.json

Large diffs are not rendered by default.

9 changes: 0 additions & 9 deletions packages/user-access-policy/ip/ip.ts

This file was deleted.

23 changes: 23 additions & 0 deletions packages/user-access-policy/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "@prosopo/user-access-policy",
"version": "1.0.0",
"author": "PROSOPO LIMITED <info@prosopo.io>",
"license": "Apache-2.0",
"type": "module",
"engines": {
"node": "20",
"npm": ">=9"
},
"scripts": {
"clean": "tsc --build --clean",
"build": "tsc --build --verbose"
},
"bugs": {
"url": "https://github.com/prosopo/captcha/issues"
},
"homepage": "https://github.com/prosopo/captcha#readme",
"sideEffects": false,
"devDependencies": {
"vite": "6.0.7"
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type ImageCaptchaConfig from "../imageCaptchaConfig/imageCaptchaConfig.js";
import type ImageCaptchaConfig from "./imageCaptcha/imageCaptchaConfig.js";

interface Config {
imageCaptcha?: ImageCaptchaConfig;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Schema } from "mongoose";
import type ImageCaptchaConfig from "./imageCaptchaConfig.js";

const mongooseImageCaptchaConfig = new Schema<ImageCaptchaConfig>(
{
solvedCount: {
type: Number,
required: false,
},
unsolvedCount: {
type: Number,
required: false,
},
},
{ _id: false },
);

export default mongooseImageCaptchaConfig;
15 changes: 15 additions & 0 deletions packages/user-access-policy/src/config/mongooseConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Schema } from "mongoose";
import type Config from "./config.js";
import MongooseImageCaptchaConfig from "./imageCaptcha/mongooseImageCaptchaConfig.js";

const mongooseConfig = new Schema<Config>(
{
imageCaptcha: {
type: MongooseImageCaptchaConfig,
required: false,
},
},
{ _id: false },
);

export default mongooseConfig;
9 changes: 9 additions & 0 deletions packages/user-access-policy/src/ip/ip.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type IpV4 from "./v4/ipV4.js";
import type IpV6 from "./v6/ipV6.js";

interface Ip {
v4?: IpV4;
v6?: IpV6;
}

export default Ip;
6 changes: 6 additions & 0 deletions packages/user-access-policy/src/ip/ipVersion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
enum IpVersion {
v4 = "v4",
v6 = "v6",
}

export default IpVersion;
34 changes: 34 additions & 0 deletions packages/user-access-policy/src/ip/mongooseIp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Schema } from "mongoose";
import type Ip from "./ip.js";
import mongooseIpV4Mask from "./v4/mask/mongooseIpV4Mask.js";
import mongooseIpV6Mask from "./v6/mask/mongooseIpV6Mask.js";

const mongooseIp = new Schema<Ip>(
{
v4: {
type: mongooseIpV4Mask,
required: [
function () {
const isV6Unset = "object" !== typeof this.v6 || null === this.v6;

return isV6Unset;
},
"v4 is required when v6 is not set",
],
},
v6: {
type: mongooseIpV6Mask,
required: [
function () {
const isV4Unset = "object" !== typeof this.v4 || null === this.v4;

return isV4Unset;
},
"v6 is required when v4 is not set",
],
},
},
{ _id: false },
);

export default mongooseIp;
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type IpV4Mask from "../ipV4Mask/ipV4Mask.js";
import type IpV4Mask from "./mask/ipV4Mask.js";

interface IpV4 {
asNumeric: bigint;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
interface UserIpV4Mask {
interface IpV4Mask {
rangeMinAsNumeric: bigint;
rangeMaxAsNumeric: bigint;
// CIDR prefix https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing - 198.51.100.14/{24}
// for presentation only purposes
asNumeric: number;
}

export default UserIpV4Mask;
export default IpV4Mask;
15 changes: 15 additions & 0 deletions packages/user-access-policy/src/ip/v4/mask/mongooseIpV4Mask.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import {Schema} from "mongoose";
import type IpV4Mask from "./ipV4Mask.js";

const mongooseIpV4Mask = new Schema<IpV4Mask>(
{
// Type choice note: Int32 can't store 10 digits of the numeric presentation of ipV4,
// so we use BigInt, which is supported by Mongoose and turned into Mongo's Long (Int64)
rangeMinAsNumeric: { type: BigInt, required: true },
rangeMaxAsNumeric: { type: BigInt, required: true },
asNumeric: { type: Number, required: true },
},
{ _id: false },
);

export default mongooseIpV4Mask;
19 changes: 19 additions & 0 deletions packages/user-access-policy/src/ip/v4/mongooseIpV4.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Schema } from "mongoose";
import type IpV4 from "./ipV4.js";
import mongooseIpV4Mask from "./mask/mongooseIpV4Mask.js";

const mongooseIpV4 = new Schema<IpV4>(
{
// Type choice note: Int32 can't store 10 digits of the numeric presentation of ipV4,
// so we use BigInt, which is supported by Mongoose and turned into Mongo's Long (Int64)
asNumeric: { type: BigInt, required: true },
asString: { type: String, required: true },
mask: {
type: mongooseIpV4Mask,
required: false,
},
},
{ _id: false },
);

export default mongooseIpV4;
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type IpV6Mask from "../ipV6Mask/ipV6Mask.js";
import type IpV6Mask from "./mask/ipV6Mask.js";

interface IpV6 {
asNumericString: string;
Expand Down
3 changes: 3 additions & 0 deletions packages/user-access-policy/src/ip/v6/ipV6NumericMaxLength.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const IPV6_NUMERIC_MAX_LENGTH = 38;

export default IPV6_NUMERIC_MAX_LENGTH;
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
interface UserIpV6Mask {
interface IpV6Mask {
rangeMinAsNumericString: string;
rangeMaxAsNumericString: string;
// CIDR prefix https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing - 2001:db8:abcd:0012:ffff:ffff:ffff:ffff/{128}
// for presentation only purposes
asNumeric: number;
}

export default UserIpV6Mask;
export default IpV6Mask;
35 changes: 35 additions & 0 deletions packages/user-access-policy/src/ip/v6/mask/mongooseIpV6Mask.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Schema } from "mongoose";
import type IpV6Mask from "./ipV6Mask.js";
import IPV6_NUMERIC_MAX_LENGTH from "../ipV6NumericMaxLength.js";

const mongooseIpV6Mask = new Schema<IpV6Mask>(
{
// 1. Type choice note:
/**
* ipV6 takes 128bits (38 digits), so we can't use Mongo's Long (Int64), and can't even Decimal128,
* cause it supports only 34 digits https://www.mongodb.com/docs/manual/reference/bson-types/
*/
// 2. String comparison note
/**
* Mongo compares strings by unicode codes of each letter, so it works for us,
* as long we make sure both strings have the exact same length:
* so '10' and '02', never '10' and '2'.
*/
rangeMinAsNumericString: {
type: String,
required: true,
// we must have the exact same string length to guarantee the right comparison.
set: (value: string): string => value.padStart(IPV6_NUMERIC_MAX_LENGTH, "0"),
},
rangeMaxAsNumericString: {
type: String,
required: true,
// we must have the exact same string length to guarantee the right comparison.
set: (value: string): string => value.padStart(IPV6_NUMERIC_MAX_LENGTH, "0"),
},
asNumeric: { type: Number, required: true },
},
{ _id: false },
);

export default mongooseIpV6Mask;
34 changes: 34 additions & 0 deletions packages/user-access-policy/src/ip/v6/mongooseIpV6.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Schema } from "mongoose";
import type IpV6 from "./ipV6.js";
import MongooseIpV6Mask from "./mask/mongooseIpV6Mask.js";
import IPV6_NUMERIC_MAX_LENGTH from "./ipV6NumericMaxLength.js";

const mongooseIpV6 = new Schema<IpV6>(
{
// 1. Type choice note:
/**
* ipV6 takes 128bits (38 digits), so we can't use Mongo's Long (Int64), and can't even Decimal128,
* cause it supports only 34 digits https://www.mongodb.com/docs/manual/reference/bson-types/
*/
// 2. String comparison note
/**
* Mongo compares strings by unicode codes of each letter, so it works for us,
* as long we make sure both strings have the exact same length:
* so '10' and '02', never '10' and '2'.
*/
asNumericString: {
type: String,
required: true,
// we must have the exact same string length to guarantee the right comparison.
set: (value: string): string => value.padStart(IPV6_NUMERIC_MAX_LENGTH, "0"),
},
asString: { type: String, required: true },
mask: {
type: MongooseIpV6Mask,
required: false,
},
},
{ _id: false },
);

export default mongooseIpV6;
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { IndexDefinition, IndexOptions } from "mongoose";

interface MongooseIndex {
definition: IndexDefinition;
options: IndexOptions;
}

export default MongooseIndex;
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import type MongooseIndex from "./mongooseIndex";

const userIpIndexes: MongooseIndex[] = [
{
definition: {
"userIp.v4.asNumeric": 1,
},
options: {
partialFilterExpression: {
"userIp.v4.asNumeric": { $exists: true },
},
},
},
{
definition: {
"userIp.v6.asNumericString": 1,
},
options: {
partialFilterExpression: {
"userIp.v6.asNumericString": { $exists: true },
},
},
},
];

const userIpMaskIndexes: MongooseIndex[] = [
{
definition: {
"userIp.v4.mask.rangeMinAsNumeric": 1,
"userIp.v4.mask.rangeMaxAsNumeric": 1,
},
options: {
partialFilterExpression: {
"userIp.v4.mask.asNumeric": { $exists: true },
},
},
},
{
definition: {
"userIp.v6.mask.rangeMinAsNumericString": 1,
"userIp.v6.mask.rangeMaxAsNumericString": 1,
},
options: {
partialFilterExpression: {
"userIp.v6.mask.asNumeric": { $exists: true },
},
},
},
];

export default [...userIpIndexes, ...userIpMaskIndexes];
Loading

0 comments on commit 6f4af7f

Please sign in to comment.