Skip to content

Commit 0360bf3

Browse files
committed
Merge remote-tracking branch 'private/master'
2 parents 087ae6e + faaa189 commit 0360bf3

File tree

11 files changed

+117
-22
lines changed

11 files changed

+117
-22
lines changed

default.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@
1515
"json": true,
1616
"colorize": false
1717
}
18+
},
19+
20+
"security": {
21+
"authToken": "-"
1822
}
1923
},
2024
"rendering": {

plugin.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,10 @@
2020
{"name": "Project site", "url": "https://github.com/grafana/grafana-image-renderer"},
2121
{"name": "Apache License", "url": "https://github.com/grafana/grafana-image-renderer/blob/master/LICENSE"}
2222
],
23-
"version": "3.5.0",
24-
"updated": "2022-07-18"
23+
"version": "3.6.0",
24+
"updated": "2022-08-16"
2525
},
2626
"dependencies": {
27-
"grafanaDependency": ">=7.0.0"
27+
"grafanaDependency": ">=8.3.11"
2828
}
2929
}

proto/rendererv2.proto

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ message RenderRequest {
1818
int32 timeout = 8;
1919
string timezone = 9;
2020
map<string, StringList> headers = 10;
21+
string authToken = 11;
2122
}
2223

2324
message RenderResponse {
@@ -32,6 +33,7 @@ message RenderCSVRequest {
3233
int32 timeout = 5;
3334
string timezone = 6;
3435
map<string, StringList> headers = 7;
36+
string authToken = 8;
3537
}
3638

3739
message RenderCSVResponse {

proto/sanitizer.proto

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ message SanitizeRequest {
88
bytes content = 2;
99
string configType = 3; // DOMPurify, ...
1010
bytes config = 4;
11+
string authToken = 5;
1112
}
1213

1314
message SanitizeResponse {

src/app.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,10 @@ function populateServiceConfigFromEnv(config: ServiceConfig, env: NodeJS.Process
108108
config.service.port = parseInt(env['HTTP_PORT'] as string, 10);
109109
}
110110

111+
if (env['AUTH_TOKEN']) {
112+
config.service.security.authToken = env['AUTH_TOKEN'];
113+
}
114+
111115
if (env['LOG_LEVEL']) {
112116
config.service.logging.level = env['LOG_LEVEL'] as string;
113117
}

src/config.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,17 @@ export interface LoggingConfig {
5454
console?: ConsoleLoggerConfig;
5555
}
5656

57+
export interface SecurityConfig {
58+
authToken: string;
59+
}
60+
5761
export interface ServiceConfig {
5862
service: {
5963
host?: string;
6064
port: number;
6165
metrics: MetricsConfig;
6266
logging: LoggingConfig;
67+
security: SecurityConfig;
6368
};
6469
rendering: RenderingConfig;
6570
}
@@ -70,6 +75,7 @@ export interface PluginConfig {
7075
host: string;
7176
port: number;
7277
};
78+
security: SecurityConfig;
7379
};
7480
rendering: RenderingConfig;
7581
}
@@ -116,6 +122,9 @@ export const defaultServiceConfig: ServiceConfig = {
116122
colorize: false,
117123
},
118124
},
125+
security: {
126+
authToken: '-',
127+
},
119128
},
120129
rendering: defaultRenderingConfig,
121130
};
@@ -126,6 +135,9 @@ export const defaultPluginConfig: PluginConfig = {
126135
host: '127.0.0.1',
127136
port: 0,
128137
},
138+
security: {
139+
authToken: '-',
140+
},
129141
},
130142
rendering: defaultRenderingConfig,
131143
};

src/plugin/v2/grpc_plugin.ts

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import * as protoLoader from '@grpc/proto-loader';
33
import * as promClient from 'prom-client';
44
import { GrpcPlugin } from '../../node-plugin';
55
import { Logger } from '../../logger';
6-
import { PluginConfig } from '../../config';
6+
import { PluginConfig, SecurityConfig } from '../../config';
77
import { createBrowser, Browser } from '../../browser';
88
import { HTTPHeaders, ImageRenderOptions, RenderOptions } from '../../types';
99
import {
@@ -21,6 +21,7 @@ import {
2121
} from './types';
2222
import { createSanitizer, Sanitizer } from '../../sanitizer/Sanitizer';
2323
import { SanitizeRequest } from '../../sanitizer/types';
24+
import { Status } from '@grpc/grpc-js/build/src/constants';
2425

2526
const rendererV2PackageDef = protoLoader.loadSync(__dirname + '/../../../proto/rendererv2.proto', {
2627
keepCase: true,
@@ -58,7 +59,7 @@ export class RenderGRPCPluginV2 implements GrpcPlugin {
5859
async grpcServer(server: grpc.Server) {
5960
const metrics = setupMetrics();
6061
const browser = createBrowser(this.config.rendering, this.log, metrics);
61-
const pluginService = new PluginGRPCServer(browser, this.log, createSanitizer());
62+
const pluginService = new PluginGRPCServer(browser, this.log, createSanitizer(), this.config.plugin.security);
6263

6364
const rendererServiceDef = rendererV2ProtoDescriptor['pluginextensionv2']['Renderer']['service'];
6465
server.addService(rendererServiceDef, pluginService as any);
@@ -92,7 +93,7 @@ export class RenderGRPCPluginV2 implements GrpcPlugin {
9293
class PluginGRPCServer {
9394
private browserVersion: string | undefined;
9495

95-
constructor(private browser: Browser, private log: Logger, private sanitizer: Sanitizer) {}
96+
constructor(private browser: Browser, private log: Logger, private sanitizer: Sanitizer, private securityCfg: SecurityConfig) {}
9697

9798
async start(browserVersion?: string) {
9899
this.browserVersion = browserVersion;
@@ -104,7 +105,16 @@ class PluginGRPCServer {
104105
const headers: HTTPHeaders = {};
105106

106107
if (!req) {
107-
throw new Error('Request cannot be null');
108+
return callback({ code: Status.INVALID_ARGUMENT, details: 'Request cannot be null' });
109+
}
110+
111+
const configToken = this.securityCfg.authToken || '';
112+
if (!req.authToken || req.authToken !== configToken) {
113+
return callback({ code: Status.UNAUTHENTICATED, details: 'Unauthorized request' });
114+
}
115+
116+
if (req.url && !(req.url.startsWith('http://') || req.url.startsWith('https://'))) {
117+
return callback({ code: Status.INVALID_ARGUMENT, details: 'Forbidden query url protocol' });
108118
}
109119

110120
if (req.headers) {
@@ -145,7 +155,16 @@ class PluginGRPCServer {
145155
const headers: HTTPHeaders = {};
146156

147157
if (!req) {
148-
throw new Error('Request cannot be null');
158+
return callback({ code: Status.INVALID_ARGUMENT, details: 'Request cannot be null' });
159+
}
160+
161+
const configToken = this.securityCfg.authToken || '';
162+
if (!req.authToken || req.authToken !== configToken) {
163+
return callback({ code: Status.UNAUTHENTICATED, details: 'Unauthorized request' });
164+
}
165+
166+
if (req.url && !(req.url.startsWith('http://') || req.url.startsWith('https://'))) {
167+
return callback({ code: Status.INVALID_ARGUMENT, details: 'Forbidden query url protocol' });
149168
}
150169

151170
if (req.headers) {
@@ -198,6 +217,11 @@ class PluginGRPCServer {
198217
async sanitize(call: grpc.ServerUnaryCall<GRPCSanitizeRequest, any>, callback: grpc.sendUnaryData<GRPCSanitizeResponse>) {
199218
const grpcReq = call.request;
200219

220+
const configToken = this.securityCfg.authToken || '';
221+
if (!grpcReq.authToken || grpcReq.authToken !== configToken) {
222+
return callback({ code: Status.UNAUTHENTICATED, details: 'Unauthorized request' });
223+
}
224+
201225
const req: SanitizeRequest = {
202226
content: grpcReq.content,
203227
config: JSON.parse(grpcReq.config.toString()),
@@ -294,6 +318,10 @@ const populateConfigFromEnv = (config: PluginConfig) => {
294318
if (env['GF_PLUGIN_RENDERING_DUMPIO']) {
295319
config.rendering.dumpio = env['GF_PLUGIN_RENDERING_DUMPIO'] === 'true';
296320
}
321+
322+
if (env['GF_PLUGIN_AUTH_TOKEN']) {
323+
config.plugin.security.authToken = env['GF_PLUGIN_AUTH_TOKEN'];
324+
}
297325
};
298326

299327
interface PluginMetrics {

src/plugin/v2/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export interface RenderRequest {
1717
headers: {
1818
[header: string]: StringList;
1919
};
20+
authToken: string;
2021
}
2122

2223
export interface RenderResponse {
@@ -33,6 +34,7 @@ export interface RenderCSVRequest {
3334
headers: {
3435
[header: string]: StringList;
3536
};
37+
authToken: string;
3638
}
3739

3840
export interface RenderCSVResponse {
@@ -72,6 +74,7 @@ export interface GRPCSanitizeRequest {
7274
configType: ConfigType;
7375
config: Buffer;
7476
allowAllLinksInSvgUseTags: boolean;
77+
authToken: string;
7578
}
7679

7780
export interface GRPCSanitizeResponse {

src/service/http-server.ts

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,14 @@ import * as promClient from 'prom-client';
88
import { Logger } from '../logger';
99
import { Browser, createBrowser } from '../browser';
1010
import { ServiceConfig } from '../config';
11-
import { setupHttpServerMetrics } from './metrics_middleware';
11+
import { setupHttpServerMetrics } from './metrics';
1212
import { HTTPHeaders, ImageRenderOptions, RenderOptions } from '../types';
1313
import { Sanitizer } from '../sanitizer/Sanitizer';
1414
import * as bodyParser from 'body-parser';
1515
import * as multer from 'multer';
1616
import { isSanitizeRequest } from '../sanitizer/types';
1717
import * as contentDisposition from 'content-disposition';
18+
import { asyncMiddleware, trustedUrlMiddleware, authTokenMiddleware } from './middlewares';
1819

1920
const upload = multer({ storage: multer.memoryStorage() });
2021

@@ -53,16 +54,26 @@ export class HttpServer {
5354
if (this.config.service.metrics.enabled) {
5455
setupHttpServerMetrics(this.app, this.config.service.metrics, this.log);
5556
}
57+
5658
this.app.get('/', (req: express.Request, res: express.Response) => {
5759
res.send('Grafana Image Renderer');
5860
});
61+
62+
// Middlewares for /render endpoints
63+
this.app.use('/render', authTokenMiddleware(this.config.service.security), trustedUrlMiddleware);
64+
65+
// Set up /render endpoints
66+
this.app.get('/render', asyncMiddleware(this.render));
67+
this.app.get('/render/csv', asyncMiddleware(this.renderCSV));
5968
this.app.get('/render/version', (req: express.Request, res: express.Response) => {
6069
const pluginInfo = require('../../plugin.json');
6170
res.send({ version: pluginInfo.info.version });
6271
});
6372

64-
this.app.get('/render', asyncMiddleware(this.render));
65-
this.app.get('/render/csv', asyncMiddleware(this.renderCSV));
73+
// Middlewares for /sanitize endpoints
74+
this.app.use('/sanitize', authTokenMiddleware(this.config.service.security));
75+
76+
// Set up /sanitize endpoints
6677
this.app.post(
6778
'/sanitize',
6879
upload.fields([
@@ -71,6 +82,7 @@ export class HttpServer {
7182
]),
7283
asyncMiddleware(this.sanitize)
7384
);
85+
7486
this.app.use((err, req, res, next) => {
7587
if (err.stack) {
7688
this.log.error('Request failed', 'url', req.url, 'stack', err.stack);
@@ -254,14 +266,3 @@ export class HttpServer {
254266
});
255267
};
256268
}
257-
258-
// wrapper for our async route handlers
259-
// probably you want to move it to a new file
260-
const asyncMiddleware = (fn) => (req, res, next) => {
261-
Promise.resolve(fn(req, res, next)).catch((err) => {
262-
if (!err.isBoom) {
263-
return next(boom.badImplementation(err));
264-
}
265-
next(err);
266-
});
267-
};
File renamed without changes.

0 commit comments

Comments
 (0)