2024-03-18 13:58:05 +01:00
|
|
|
import {
|
|
|
|
type IRouter,
|
|
|
|
Router,
|
|
|
|
type Request,
|
|
|
|
type Response,
|
|
|
|
type RequestHandler,
|
|
|
|
} from 'express';
|
|
|
|
import type { Logger } from '../logger';
|
|
|
|
import { type IUnleashConfig, NONE } from '../types';
|
2021-08-13 10:36:19 +02:00
|
|
|
import { handleErrors } from './util';
|
2021-12-03 12:46:50 +01:00
|
|
|
import requireContentType from '../middleware/content_type_checker';
|
2023-07-10 12:48:13 +02:00
|
|
|
import { PermissionError } from '../error';
|
fix: path metric labels (#6400)
## About the changes
Some of our metrics are not labeled correctly, one example is
`<base-path>/api/frontend/client/metrics` is labeled as
`/client/metrics`. We can see that in internal-backstage/prometheus:
![image](https://github.com/Unleash/unleash/assets/455064/0d8f1f40-8b5b-49d4-8a88-70b523e9be09)
This issue affects all endpoints that fail to validate the request body.
Also, endpoints that are rejected by the authorization-middleware or the
api-token-middleware are reported as `(hidden)`.
To gain more insights on our api usage but being protective of metrics
cardinality we're prefixing `(hidden)` with some well known base urls:
https://github.com/Unleash/unleash/pull/6400/files#diff-1ed998ca46ffc97c9c0d5d400bfd982dbffdb3004b78a230a8a38e7644eee9b6R17-R33
## How to reproduce:
Make an invalid call to metrics (e.g. stop set to null), then check
/internal-backstage/prometheus and find the 400 error. Expected to be at
`path="/api/client/metrics"` but will have `path=""`:
```shell
curl -H"Authorization: *:development.unleash-insecure-client-api-token" -H'Content-type: application/json' localhost:4242/api/client/metrics -d '{
"appName": "bash-test",
"instanceId": "application-name-dacb1234",
"environment": "development",
"bucket": {
"start": "2023-07-27T11:23:44Z",
"stop": null,
"toggles": {
"myCoolToggle": {
"yes": 25,
"no": 42,
"variants": {
"blue": 6,
"green": 15,
"red": 46
}
},
"myOtherToggle": {
"yes": 0,
"no": 100
}
}
}
}'
```
2024-03-05 15:25:06 +01:00
|
|
|
import { storeRequestedRoute } from '../middleware/response-time-metrics';
|
2021-04-12 20:25:03 +02:00
|
|
|
|
2024-03-18 13:58:05 +01:00
|
|
|
type IRequestHandler<P = any, ResBody = any, ReqBody = any, ReqQuery = any> = (
|
|
|
|
req: Request<P, ResBody, ReqBody, ReqQuery>,
|
|
|
|
res: Response<ResBody>,
|
|
|
|
) => Promise<void> | void;
|
2021-08-13 10:36:19 +02:00
|
|
|
|
2023-06-22 09:35:54 +02:00
|
|
|
type Permission = string | string[];
|
|
|
|
|
2022-06-07 09:32:18 +02:00
|
|
|
interface IRouteOptionsBase {
|
2022-04-25 14:17:59 +02:00
|
|
|
path: string;
|
2023-06-22 09:35:54 +02:00
|
|
|
permission: Permission;
|
2022-04-25 14:17:59 +02:00
|
|
|
middleware?: RequestHandler[];
|
|
|
|
handler: IRequestHandler;
|
|
|
|
acceptedContentTypes?: string[];
|
|
|
|
}
|
|
|
|
|
2022-06-07 09:32:18 +02:00
|
|
|
interface IRouteOptionsGet extends IRouteOptionsBase {
|
|
|
|
method: 'get';
|
|
|
|
}
|
|
|
|
|
|
|
|
interface IRouteOptionsNonGet extends IRouteOptionsBase {
|
|
|
|
method: 'post' | 'put' | 'patch' | 'delete';
|
|
|
|
acceptAnyContentType?: boolean;
|
|
|
|
}
|
|
|
|
|
|
|
|
type IRouteOptions = IRouteOptionsNonGet | IRouteOptionsGet;
|
|
|
|
|
2023-06-22 09:35:54 +02:00
|
|
|
const checkPermission =
|
2024-01-12 10:25:59 +01:00
|
|
|
(permission: Permission = []) =>
|
|
|
|
async (req, res, next) => {
|
2023-06-22 09:35:54 +02:00
|
|
|
const permissions = (
|
|
|
|
Array.isArray(permission) ? permission : [permission]
|
|
|
|
).filter((p) => p !== NONE);
|
|
|
|
|
|
|
|
if (!permissions.length) {
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
if (req.checkRbac && (await req.checkRbac(permissions))) {
|
|
|
|
return next();
|
|
|
|
}
|
2023-07-10 12:48:13 +02:00
|
|
|
return res.status(403).json(new PermissionError(permissions)).end();
|
2023-06-22 09:35:54 +02:00
|
|
|
};
|
2021-04-12 20:25:03 +02:00
|
|
|
|
2023-09-15 14:52:54 +02:00
|
|
|
const checkPrivateProjectPermissions = () => async (req, res, next) => {
|
|
|
|
if (
|
|
|
|
!req.checkPrivateProjectPermissions ||
|
|
|
|
(await req.checkPrivateProjectPermissions())
|
|
|
|
) {
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
return res.status(404).end();
|
|
|
|
};
|
|
|
|
|
2018-11-30 10:11:36 +01:00
|
|
|
/**
|
|
|
|
* Base class for Controllers to standardize binding to express Router.
|
2021-08-13 10:36:19 +02:00
|
|
|
*
|
|
|
|
* This class will take care of the following:
|
|
|
|
* - try/catch inside RequestHandler
|
|
|
|
* - await if the RequestHandler returns a promise.
|
|
|
|
* - access control
|
2018-11-30 10:11:36 +01:00
|
|
|
*/
|
2021-04-22 10:07:10 +02:00
|
|
|
export default class Controller {
|
2021-08-13 10:36:19 +02:00
|
|
|
private ownLogger: Logger;
|
|
|
|
|
2021-04-22 10:07:10 +02:00
|
|
|
app: IRouter;
|
|
|
|
|
|
|
|
config: IUnleashConfig;
|
|
|
|
|
|
|
|
constructor(config: IUnleashConfig) {
|
2021-08-13 10:36:19 +02:00
|
|
|
this.ownLogger = config.getLogger(
|
|
|
|
`controller/${this.constructor.name}`,
|
|
|
|
);
|
2021-04-22 10:07:10 +02:00
|
|
|
this.app = Router();
|
2018-12-19 14:50:01 +01:00
|
|
|
this.config = config;
|
2018-12-19 13:35:54 +01:00
|
|
|
}
|
|
|
|
|
2022-04-25 14:17:59 +02:00
|
|
|
private useRouteErrorHandler(handler: IRequestHandler): IRequestHandler {
|
2021-08-13 10:36:19 +02:00
|
|
|
return async (req: Request, res: Response) => {
|
|
|
|
try {
|
|
|
|
await handler(req, res);
|
|
|
|
} catch (error) {
|
|
|
|
handleErrors(res, this.ownLogger, error);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2022-04-25 14:17:59 +02:00
|
|
|
private useContentTypeMiddleware(options: IRouteOptions): RequestHandler[] {
|
|
|
|
const { middleware = [], acceptedContentTypes = [] } = options;
|
|
|
|
|
2022-06-07 09:32:18 +02:00
|
|
|
return options.method === 'get' || options.acceptAnyContentType
|
2022-04-25 14:17:59 +02:00
|
|
|
? middleware
|
|
|
|
: [requireContentType(...acceptedContentTypes), ...middleware];
|
|
|
|
}
|
|
|
|
|
|
|
|
route(options: IRouteOptions): void {
|
|
|
|
this.app[options.method](
|
|
|
|
options.path,
|
fix: path metric labels (#6400)
## About the changes
Some of our metrics are not labeled correctly, one example is
`<base-path>/api/frontend/client/metrics` is labeled as
`/client/metrics`. We can see that in internal-backstage/prometheus:
![image](https://github.com/Unleash/unleash/assets/455064/0d8f1f40-8b5b-49d4-8a88-70b523e9be09)
This issue affects all endpoints that fail to validate the request body.
Also, endpoints that are rejected by the authorization-middleware or the
api-token-middleware are reported as `(hidden)`.
To gain more insights on our api usage but being protective of metrics
cardinality we're prefixing `(hidden)` with some well known base urls:
https://github.com/Unleash/unleash/pull/6400/files#diff-1ed998ca46ffc97c9c0d5d400bfd982dbffdb3004b78a230a8a38e7644eee9b6R17-R33
## How to reproduce:
Make an invalid call to metrics (e.g. stop set to null), then check
/internal-backstage/prometheus and find the 400 error. Expected to be at
`path="/api/client/metrics"` but will have `path=""`:
```shell
curl -H"Authorization: *:development.unleash-insecure-client-api-token" -H'Content-type: application/json' localhost:4242/api/client/metrics -d '{
"appName": "bash-test",
"instanceId": "application-name-dacb1234",
"environment": "development",
"bucket": {
"start": "2023-07-27T11:23:44Z",
"stop": null,
"toggles": {
"myCoolToggle": {
"yes": 25,
"no": 42,
"variants": {
"blue": 6,
"green": 15,
"red": 46
}
},
"myOtherToggle": {
"yes": 0,
"no": 100
}
}
}
}'
```
2024-03-05 15:25:06 +01:00
|
|
|
storeRequestedRoute,
|
2022-04-25 14:17:59 +02:00
|
|
|
checkPermission(options.permission),
|
2023-09-15 14:52:54 +02:00
|
|
|
checkPrivateProjectPermissions(),
|
2022-04-25 14:17:59 +02:00
|
|
|
this.useContentTypeMiddleware(options),
|
|
|
|
this.useRouteErrorHandler(options.handler.bind(this)),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2023-06-22 09:35:54 +02:00
|
|
|
get(
|
|
|
|
path: string,
|
|
|
|
handler: IRequestHandler,
|
|
|
|
permission: Permission = NONE,
|
|
|
|
): void {
|
2022-04-25 14:17:59 +02:00
|
|
|
this.route({
|
|
|
|
method: 'get',
|
2021-08-13 10:36:19 +02:00
|
|
|
path,
|
2022-04-25 14:17:59 +02:00
|
|
|
handler,
|
|
|
|
permission,
|
|
|
|
});
|
2018-11-30 10:11:36 +01:00
|
|
|
}
|
|
|
|
|
2021-04-22 10:07:10 +02:00
|
|
|
post(
|
|
|
|
path: string,
|
2021-08-13 10:36:19 +02:00
|
|
|
handler: IRequestHandler,
|
2023-06-22 09:35:54 +02:00
|
|
|
permission: Permission = NONE,
|
2021-04-22 10:07:10 +02:00
|
|
|
...acceptedContentTypes: string[]
|
|
|
|
): void {
|
2022-04-25 14:17:59 +02:00
|
|
|
this.route({
|
|
|
|
method: 'post',
|
2018-12-19 13:35:54 +01:00
|
|
|
path,
|
2022-04-25 14:17:59 +02:00
|
|
|
handler,
|
|
|
|
permission,
|
|
|
|
acceptedContentTypes,
|
|
|
|
});
|
2018-11-30 10:11:36 +01:00
|
|
|
}
|
|
|
|
|
2021-04-22 10:07:10 +02:00
|
|
|
put(
|
|
|
|
path: string,
|
2021-08-13 10:36:19 +02:00
|
|
|
handler: IRequestHandler,
|
2023-06-22 09:35:54 +02:00
|
|
|
permission: Permission = NONE,
|
2021-04-22 10:07:10 +02:00
|
|
|
...acceptedContentTypes: string[]
|
|
|
|
): void {
|
2022-04-25 14:17:59 +02:00
|
|
|
this.route({
|
|
|
|
method: 'put',
|
2018-12-19 13:35:54 +01:00
|
|
|
path,
|
2022-04-25 14:17:59 +02:00
|
|
|
handler,
|
|
|
|
permission,
|
|
|
|
acceptedContentTypes,
|
|
|
|
});
|
2018-11-30 11:11:12 +01:00
|
|
|
}
|
|
|
|
|
2021-09-13 10:23:57 +02:00
|
|
|
patch(
|
|
|
|
path: string,
|
|
|
|
handler: IRequestHandler,
|
2023-06-22 09:35:54 +02:00
|
|
|
permission: Permission = NONE,
|
2021-09-13 10:23:57 +02:00
|
|
|
...acceptedContentTypes: string[]
|
|
|
|
): void {
|
2022-04-25 14:17:59 +02:00
|
|
|
this.route({
|
|
|
|
method: 'patch',
|
2021-09-13 10:23:57 +02:00
|
|
|
path,
|
2022-04-25 14:17:59 +02:00
|
|
|
handler,
|
|
|
|
permission,
|
|
|
|
acceptedContentTypes,
|
|
|
|
});
|
2021-09-13 10:23:57 +02:00
|
|
|
}
|
|
|
|
|
2023-06-22 09:35:54 +02:00
|
|
|
delete(
|
|
|
|
path: string,
|
|
|
|
handler: IRequestHandler,
|
|
|
|
permission: Permission = NONE,
|
|
|
|
): void {
|
2022-04-25 14:17:59 +02:00
|
|
|
this.route({
|
|
|
|
method: 'delete',
|
2021-08-13 10:36:19 +02:00
|
|
|
path,
|
2022-04-25 14:17:59 +02:00
|
|
|
handler,
|
|
|
|
permission,
|
|
|
|
acceptAnyContentType: true,
|
|
|
|
});
|
2018-11-30 11:11:12 +01:00
|
|
|
}
|
|
|
|
|
2021-04-22 10:07:10 +02:00
|
|
|
fileupload(
|
|
|
|
path: string,
|
2021-08-13 10:36:19 +02:00
|
|
|
filehandler: IRequestHandler,
|
2021-04-22 10:07:10 +02:00
|
|
|
handler: Function,
|
2023-06-22 09:35:54 +02:00
|
|
|
permission: Permission = NONE,
|
2021-04-22 10:07:10 +02:00
|
|
|
): void {
|
2019-03-13 19:10:13 +01:00
|
|
|
this.app.post(
|
|
|
|
path,
|
fix: path metric labels (#6400)
## About the changes
Some of our metrics are not labeled correctly, one example is
`<base-path>/api/frontend/client/metrics` is labeled as
`/client/metrics`. We can see that in internal-backstage/prometheus:
![image](https://github.com/Unleash/unleash/assets/455064/0d8f1f40-8b5b-49d4-8a88-70b523e9be09)
This issue affects all endpoints that fail to validate the request body.
Also, endpoints that are rejected by the authorization-middleware or the
api-token-middleware are reported as `(hidden)`.
To gain more insights on our api usage but being protective of metrics
cardinality we're prefixing `(hidden)` with some well known base urls:
https://github.com/Unleash/unleash/pull/6400/files#diff-1ed998ca46ffc97c9c0d5d400bfd982dbffdb3004b78a230a8a38e7644eee9b6R17-R33
## How to reproduce:
Make an invalid call to metrics (e.g. stop set to null), then check
/internal-backstage/prometheus and find the 400 error. Expected to be at
`path="/api/client/metrics"` but will have `path=""`:
```shell
curl -H"Authorization: *:development.unleash-insecure-client-api-token" -H'Content-type: application/json' localhost:4242/api/client/metrics -d '{
"appName": "bash-test",
"instanceId": "application-name-dacb1234",
"environment": "development",
"bucket": {
"start": "2023-07-27T11:23:44Z",
"stop": null,
"toggles": {
"myCoolToggle": {
"yes": 25,
"no": 42,
"variants": {
"blue": 6,
"green": 15,
"red": 46
}
},
"myOtherToggle": {
"yes": 0,
"no": 100
}
}
}
}'
```
2024-03-05 15:25:06 +01:00
|
|
|
storeRequestedRoute,
|
2021-04-12 20:25:03 +02:00
|
|
|
checkPermission(permission),
|
2023-09-15 14:52:54 +02:00
|
|
|
checkPrivateProjectPermissions(),
|
2021-04-22 10:07:10 +02:00
|
|
|
filehandler.bind(this),
|
2022-04-25 14:17:59 +02:00
|
|
|
this.useRouteErrorHandler(handler.bind(this)),
|
2019-03-13 19:10:13 +01:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-04-22 10:07:10 +02:00
|
|
|
use(path: string, router: IRouter): void {
|
2018-12-03 08:59:13 +01:00
|
|
|
this.app.use(path, router);
|
|
|
|
}
|
|
|
|
|
2022-09-26 09:58:58 +02:00
|
|
|
useWithMiddleware(path: string, router: IRouter, middleware: any): void {
|
|
|
|
this.app.use(path, middleware, router);
|
|
|
|
}
|
|
|
|
|
fix: path metric labels (#6400)
## About the changes
Some of our metrics are not labeled correctly, one example is
`<base-path>/api/frontend/client/metrics` is labeled as
`/client/metrics`. We can see that in internal-backstage/prometheus:
![image](https://github.com/Unleash/unleash/assets/455064/0d8f1f40-8b5b-49d4-8a88-70b523e9be09)
This issue affects all endpoints that fail to validate the request body.
Also, endpoints that are rejected by the authorization-middleware or the
api-token-middleware are reported as `(hidden)`.
To gain more insights on our api usage but being protective of metrics
cardinality we're prefixing `(hidden)` with some well known base urls:
https://github.com/Unleash/unleash/pull/6400/files#diff-1ed998ca46ffc97c9c0d5d400bfd982dbffdb3004b78a230a8a38e7644eee9b6R17-R33
## How to reproduce:
Make an invalid call to metrics (e.g. stop set to null), then check
/internal-backstage/prometheus and find the 400 error. Expected to be at
`path="/api/client/metrics"` but will have `path=""`:
```shell
curl -H"Authorization: *:development.unleash-insecure-client-api-token" -H'Content-type: application/json' localhost:4242/api/client/metrics -d '{
"appName": "bash-test",
"instanceId": "application-name-dacb1234",
"environment": "development",
"bucket": {
"start": "2023-07-27T11:23:44Z",
"stop": null,
"toggles": {
"myCoolToggle": {
"yes": 25,
"no": 42,
"variants": {
"blue": 6,
"green": 15,
"red": 46
}
},
"myOtherToggle": {
"yes": 0,
"no": 100
}
}
}
}'
```
2024-03-05 15:25:06 +01:00
|
|
|
get router(): IRouter {
|
2018-11-30 10:11:36 +01:00
|
|
|
return this.app;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
module.exports = Controller;
|