feat: support postgres store for rate limiting

Signed-off-by: Hellgren Heikki <heikki.hellgren@op.fi>
This commit is contained in:
Hellgren Heikki
2025-02-04 08:06:37 +02:00
parent d6bd7a540d
commit c05a6982ea
7 changed files with 184 additions and 5 deletions
+2 -2
View File
@@ -4,12 +4,12 @@
Added new rate limit middleware to allow rate limiting requests to the backend
If you are using the `configure` callback of the root HTTP router service and do NOT call `applyDefaults()` inside it, please see [the relevant changes](https://github.com/backstage/backstage/pull/26725/files#diff-86ad1b6a694dd250823aee39d410428dd837c9d9a04ca8c33bd1081fbe3f22af) that were made, to see if you want to apply them as well to your custom configuration.
If you are using the `configure` callback of the root HTTP router service and do NOT call `applyDefaults()` inside it, please see [the relevant changes](https://github.com/backstage/backstage/pull/28708/files#diff-86ad1b6a694dd250823aee39d410428dd837c9d9a04ca8c33bd1081fbe3f22af) that were made, to see if you want to apply them as well to your custom configuration.
Rate limiting can be turned on by adding the following configuration to `app-config.yaml`:
```yaml
backend:
rateLimit:
window: 6000ms
window: 6s
incomingRequestLimit: 100
```
+1 -1
View File
@@ -38,7 +38,7 @@ backend:
# Used for testing rate limiting locally
# rateLimit:
# windowMs: 60000
# windowMs: 1m
# incomingRequestLimit: 1
# ipAllowList: []
+16
View File
@@ -801,6 +801,22 @@ export interface Config {
client: 'redis';
connection: string;
}
| {
client: 'postgres';
connection:
| string
| {
/**
* @visibility secret
*/
password?: string;
/**
* Other connection settings
* @see https://node-postgres.com/apis/client
*/
[key: string]: unknown;
};
}
| {
client: 'memory';
};
+1
View File
@@ -130,6 +130,7 @@
"test": "backstage-cli package test"
},
"dependencies": {
"@acpr/rate-limit-postgresql": "^1.4.1",
"@aws-sdk/abort-controller": "^3.347.0",
"@aws-sdk/client-codecommit": "^3.350.0",
"@aws-sdk/client-s3": "^3.350.0",
@@ -15,6 +15,8 @@
*/
import { mockServices } from '@backstage/backend-test-utils';
import { RateLimitStoreFactory } from './RateLimitStoreFactory';
import { RedisStore } from 'rate-limit-redis';
import { PostgresStore } from '@acpr/rate-limit-postgresql';
jest.mock('@keyv/redis', () => {
const Actual = jest.requireActual('@keyv/redis');
@@ -62,6 +64,23 @@ describe('CacheRateLimitStoreFactory', () => {
},
});
const store = RateLimitStoreFactory.create(config);
expect(store).not.toBeUndefined();
expect(store).toBeInstanceOf(RedisStore);
});
it('should return postgres store if configured explicitly', async () => {
const config = mockServices.rootConfig({
data: {
backend: {
rateLimit: {
store: {
client: 'postgres',
connection: 'postgres://localhost:5432',
},
},
},
},
});
const store = RateLimitStoreFactory.create(config);
expect(store).toBeInstanceOf(PostgresStore);
});
});
@@ -16,6 +16,8 @@
import { Config } from '@backstage/config';
import type { Store } from 'express-rate-limit';
import { RedisStore } from 'rate-limit-redis';
import { parsePgConnectionString } from '../../database/connectors/postgres.ts';
import { PostgresStore } from '@acpr/rate-limit-postgresql';
/**
* Creates a store for `express-rate-limit` based on the configuration.
@@ -32,6 +34,9 @@ export class RateLimitStoreFactory {
switch (client) {
case 'redis':
return this.redis(store);
case 'postgres':
return this.postgres(store);
case 'memory':
default:
return undefined;
}
@@ -48,4 +53,14 @@ export class RateLimitStoreFactory {
},
});
}
private static postgres(storeConfig: Config): Store {
const connection = storeConfig.get('connection') as any;
const isConnectionString =
typeof connection === 'string' || connection instanceof String;
const connectionOptions = isConnectionString
? parsePgConnectionString(connection as string)
: connection;
return new PostgresStore(connectionOptions, 'rl');
}
}
+129 -1
View File
@@ -12,6 +12,20 @@ __metadata:
languageName: node
linkType: hard
"@acpr/rate-limit-postgresql@npm:^1.4.1":
version: 1.4.1
resolution: "@acpr/rate-limit-postgresql@npm:1.4.1"
dependencies:
"@types/pg-pool": "npm:2.0.3"
pg: "npm:8.11.3"
pg-pool: "npm:3.6.1"
postgres-migrations: "npm:5.3.0"
peerDependencies:
express-rate-limit: ">=6.0.0"
checksum: 10/9295f86890ea10f0be24a211f100cfe9dde40df20d8328be36a66736e36ee7043dc6fcae785e39bea19de3f43ad344f3e0fa3f9d40bc8d89d38bf6ce457bcc28
languageName: node
linkType: hard
"@adobe/css-tools@npm:^4.4.0":
version: 4.4.0
resolution: "@adobe/css-tools@npm:4.4.0"
@@ -3580,6 +3594,7 @@ __metadata:
version: 0.0.0-use.local
resolution: "@backstage/backend-defaults@workspace:packages/backend-defaults"
dependencies:
"@acpr/rate-limit-postgresql": "npm:^1.4.1"
"@aws-sdk/abort-controller": "npm:^3.347.0"
"@aws-sdk/client-codecommit": "npm:^3.350.0"
"@aws-sdk/client-s3": "npm:^3.350.0"
@@ -20987,6 +21002,15 @@ __metadata:
languageName: node
linkType: hard
"@types/pg-pool@npm:2.0.3":
version: 2.0.3
resolution: "@types/pg-pool@npm:2.0.3"
dependencies:
"@types/pg": "npm:*"
checksum: 10/9ea0bcdbdd09c9de6f774e59465189e552ee094901724278082c41ba6287e7fddffb9ba4b4107c242bba4e8f8a1f0016e6a1eb0c6ca306d43c08b5ddd7f34549
languageName: node
linkType: hard
"@types/pg-pool@npm:2.0.6":
version: 2.0.6
resolution: "@types/pg-pool@npm:2.0.6"
@@ -25149,6 +25173,13 @@ __metadata:
languageName: node
linkType: hard
"buffer-writer@npm:2.0.0":
version: 2.0.0
resolution: "buffer-writer@npm:2.0.0"
checksum: 10/fdca8e28c55704de7af2f41c8f875293de69ad22005d5041d54aa916d125cead00afa969bc09e4702ae6b66e098409958c06bebfc97fcf8fa4ea5afcae088cd9
languageName: node
linkType: hard
"buffer-xor@npm:^1.0.3":
version: 1.0.3
resolution: "buffer-xor@npm:1.0.3"
@@ -39963,6 +39994,13 @@ __metadata:
languageName: node
linkType: hard
"packet-reader@npm:1.0.0":
version: 1.0.0
resolution: "packet-reader@npm:1.0.0"
checksum: 10/8504cc8c32672380867e933516a029b1d4dd784c139213c85c9042ffc1162de48ec914f8c71260a9311518694cf5d0be11c67357f4b536129d2ea42aa7257ec0
languageName: node
linkType: hard
"pacote@npm:^12.0.0, pacote@npm:^12.0.2":
version: 12.0.3
resolution: "pacote@npm:12.0.3"
@@ -40492,7 +40530,7 @@ __metadata:
languageName: node
linkType: hard
"pg-connection-string@npm:^2.3.0, pg-connection-string@npm:^2.5.0, pg-connection-string@npm:^2.7.0":
"pg-connection-string@npm:^2.3.0, pg-connection-string@npm:^2.5.0, pg-connection-string@npm:^2.6.2, pg-connection-string@npm:^2.7.0":
version: 2.7.0
resolution: "pg-connection-string@npm:2.7.0"
checksum: 10/68015a8874b7ca5dad456445e4114af3d2602bac2fdb8069315ecad0ff9660ec93259b9af7186606529ac4f6f72a06831e6f20897a689b16cc7fda7ca0e247fd
@@ -40520,6 +40558,24 @@ __metadata:
languageName: node
linkType: hard
"pg-pool@npm:3.6.1":
version: 3.6.1
resolution: "pg-pool@npm:3.6.1"
peerDependencies:
pg: ">=8.0"
checksum: 10/5d1b02b959e6c849004d8f3d2222c48d3b3b67b7b1eb5f2e5819ed9412129ea6b0f0376bc74ddf197973c99575d325cbb3f64a8017ab520535c011329b12fffb
languageName: node
linkType: hard
"pg-pool@npm:^3.6.1, pg-pool@npm:^3.7.0":
version: 3.7.0
resolution: "pg-pool@npm:3.7.0"
peerDependencies:
pg: ">=8.0"
checksum: 10/a07a4f9e26eec9d7ac3597dc7b3469c62983edff9a321dbb7acbe1bbc7f5e9b2d33438e277d4cf8145071f3d63c7ebdc287a539fd69dfb8cdddb15b33eefe1a2
languageName: node
linkType: hard
"pg-pool@npm:^3.8.0":
version: 3.8.0
resolution: "pg-pool@npm:3.8.0"
@@ -40536,6 +40592,13 @@ __metadata:
languageName: node
linkType: hard
"pg-protocol@npm:^1.6.0, pg-protocol@npm:^1.7.0":
version: 1.7.0
resolution: "pg-protocol@npm:1.7.0"
checksum: 10/ffffdf74426c9357b57050f1c191e84447c0e8b2a701b3ab302ac7dd0eb27b862d92e5e3b2d38876a1051de83547eb9165d6a58b3a8e90bb050dae97f9993d54
languageName: node
linkType: hard
"pg-types@npm:^2.1.0, pg-types@npm:^2.2.0":
version: 2.2.0
resolution: "pg-types@npm:2.2.0"
@@ -40564,6 +40627,30 @@ __metadata:
languageName: node
linkType: hard
"pg@npm:8.11.3":
version: 8.11.3
resolution: "pg@npm:8.11.3"
dependencies:
buffer-writer: "npm:2.0.0"
packet-reader: "npm:1.0.0"
pg-cloudflare: "npm:^1.1.1"
pg-connection-string: "npm:^2.6.2"
pg-pool: "npm:^3.6.1"
pg-protocol: "npm:^1.6.0"
pg-types: "npm:^2.1.0"
pgpass: "npm:1.x"
peerDependencies:
pg-native: ">=3.0.1"
dependenciesMeta:
pg-cloudflare:
optional: true
peerDependenciesMeta:
pg-native:
optional: true
checksum: 10/f15f29c8e17723ee1da72abdf400cbed2c04602c58c93687f3f0068e71df2a6fb62b9a3543e13da21b10a0494f4c5b4cfc8d6cd8396617b76c4cbfd6ddab17e7
languageName: node
linkType: hard
"pg@npm:^8.11.3, pg@npm:^8.9.0":
version: 8.14.1
resolution: "pg@npm:8.14.1"
@@ -40586,6 +40673,28 @@ __metadata:
languageName: node
linkType: hard
"pg@npm:^8.6.0":
version: 8.13.1
resolution: "pg@npm:8.13.1"
dependencies:
pg-cloudflare: "npm:^1.1.1"
pg-connection-string: "npm:^2.7.0"
pg-pool: "npm:^3.7.0"
pg-protocol: "npm:^1.7.0"
pg-types: "npm:^2.1.0"
pgpass: "npm:1.x"
peerDependencies:
pg-native: ">=3.0.1"
dependenciesMeta:
pg-cloudflare:
optional: true
peerDependenciesMeta:
pg-native:
optional: true
checksum: 10/542aa49fcb37657cf5f779b4a31fe6eb336e683445ecca38e267eeb0ca85d873ffe51f04794f9f9e184187e9f74bf7895e932a0fa9507132ac0dfc76c7c73451
languageName: node
linkType: hard
"pgpass@npm:1.x":
version: 1.0.2
resolution: "pgpass@npm:1.0.2"
@@ -41353,6 +41462,18 @@ __metadata:
languageName: node
linkType: hard
"postgres-migrations@npm:5.3.0":
version: 5.3.0
resolution: "postgres-migrations@npm:5.3.0"
dependencies:
pg: "npm:^8.6.0"
sql-template-strings: "npm:^2.2.2"
bin:
pg-validate-migrations: dist/bin/validate.js
checksum: 10/520d95f01144f88689d5c0a7575743c4f99536935deb1ffff7b3765883a688c4f001d98e8b493ca9b342cd2609593970c3d2198b41fade648f102008e3607226
languageName: node
linkType: hard
"postgres-range@npm:^1.1.1":
version: 1.1.3
resolution: "postgres-range@npm:1.1.3"
@@ -45172,6 +45293,13 @@ __metadata:
languageName: node
linkType: hard
"sql-template-strings@npm:^2.2.2":
version: 2.2.2
resolution: "sql-template-strings@npm:2.2.2"
checksum: 10/594378a44acbaf3db8a4067137c0c315d0656fcc1b6b8fa76c760d032c1970bf6ede2b31690a3bdc6482d86cbff8b202bb14f6528aa1d9d6bf19d48b03ba2744
languageName: node
linkType: hard
"sqlstring@npm:^2.3.2":
version: 2.3.2
resolution: "sqlstring@npm:2.3.2"