mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	fix: add middleware verifying content type
- By default only accepts 'application/json' - Routes that need different content-type, can call post or put with additional arguments, one per content-type you need to support.
This commit is contained in:
		
							parent
							
								
									dcde06b0c3
								
							
						
					
					
						commit
						6c2af3c6bc
					
				
							
								
								
									
										30
									
								
								lib/middleware/content_type_checker.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								lib/middleware/content_type_checker.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,30 @@ | ||||
| const DEFAULT_ACCEPTED_CONTENT_TYPE = 'application/json'; | ||||
| 
 | ||||
| /** | ||||
|  * Builds an express middleware checking the content-type header | ||||
|  * returning 415 if the header is not either `application/json` or in the array | ||||
|  * passed into the function of valid content-types | ||||
|  * @param {String} acceptedContentTypes | ||||
|  * @returns {function(Request, Response, NextFunction): void} | ||||
|  */ | ||||
| function requireContentType(...acceptedContentTypes) { | ||||
|     return (req, res, next) => { | ||||
|         const contentType = req.header('Content-Type'); | ||||
|         if ( | ||||
|             Array.isArray(acceptedContentTypes) && | ||||
|             acceptedContentTypes.length > 0 | ||||
|         ) { | ||||
|             if (acceptedContentTypes.includes(contentType)) { | ||||
|                 next(); | ||||
|             } else { | ||||
|                 res.status(415).end(); | ||||
|             } | ||||
|         } else if (DEFAULT_ACCEPTED_CONTENT_TYPE === contentType) { | ||||
|             next(); | ||||
|         } else { | ||||
|             res.status(415).end(); | ||||
|         } | ||||
|     }; | ||||
| } | ||||
| 
 | ||||
| module.exports = requireContentType; | ||||
							
								
								
									
										63
									
								
								lib/middleware/content_type_checker.test.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								lib/middleware/content_type_checker.test.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,63 @@ | ||||
| const test = require('ava'); | ||||
| 
 | ||||
| const requireContentType = require('./content_type_checker'); | ||||
| 
 | ||||
| const mockRequest = contentType => ({ | ||||
|     header: name => { | ||||
|         if (name === 'Content-Type') { | ||||
|             return contentType; | ||||
|         } | ||||
|         return ''; | ||||
|     }, | ||||
| }); | ||||
| 
 | ||||
| const returns415 = t => { | ||||
|     return { | ||||
|         status: code => { | ||||
|             t.is(415, code); | ||||
|             return { | ||||
|                 end: t.pass, | ||||
|             }; | ||||
|         }, | ||||
|     }; | ||||
| }; | ||||
| 
 | ||||
| const expectNoCall = t => { | ||||
|     return { | ||||
|         status: () => { | ||||
|             return { | ||||
|                 end: t.fail, | ||||
|             }; | ||||
|         }, | ||||
|     }; | ||||
| }; | ||||
| 
 | ||||
| test('Content-type middleware should by default only support application/json', t => { | ||||
|     const middleware = requireContentType(); | ||||
|     middleware(mockRequest('application/json'), expectNoCall(t), t.pass); | ||||
|     middleware(mockRequest('text/plain'), returns415(t), t.fail); | ||||
| }); | ||||
| 
 | ||||
| test('Content-type middleware should allow adding custom supported types', t => { | ||||
|     const middleware = requireContentType('application/yaml'); | ||||
|     middleware(mockRequest('application/yaml'), expectNoCall(t), t.pass); | ||||
|     middleware(mockRequest('text/html'), returns415(t), t.fail); | ||||
|     middleware(mockRequest('text/plain'), returns415(t), t.fail); | ||||
| }); | ||||
| 
 | ||||
| test('adding custom supported types no longer supports default type', t => { | ||||
|     const middleware = requireContentType('application/yaml'); | ||||
|     middleware(mockRequest('application/json'), returns415(t), t.fail); | ||||
| }); | ||||
| 
 | ||||
| test('Should be able to add multiple content-types supported', t => { | ||||
|     const middleware = requireContentType( | ||||
|         'application/json', | ||||
|         'application/yaml', | ||||
|         'form/multipart', | ||||
|     ); | ||||
|     middleware(mockRequest('application/json'), expectNoCall(t), t.pass); | ||||
|     middleware(mockRequest('application/yaml'), expectNoCall(t), t.pass); | ||||
|     middleware(mockRequest('form/multipart'), expectNoCall(t), t.pass); | ||||
|     middleware(mockRequest('text/plain'), returns415(t), t.fail); | ||||
| }); | ||||
| @ -79,7 +79,10 @@ test('should revive toggle', t => { | ||||
|         strategies: [{ name: 'default' }], | ||||
|     }); | ||||
| 
 | ||||
|     return request.post(`${base}/api/admin/archive/revive/${name}`).expect(200); | ||||
|     return request | ||||
|         .post(`${base}/api/admin/archive/revive/${name}`) | ||||
|         .set('Content-Type', 'application/json') | ||||
|         .expect(200); | ||||
| }); | ||||
| 
 | ||||
| test('should create event when reviving toggle', async t => { | ||||
| @ -108,7 +111,9 @@ test('should create event when reviving toggle', async t => { | ||||
|         'test@test.com', | ||||
|     ); | ||||
| 
 | ||||
|     await request.post(`${base}/api/admin/archive/revive/${name}`); | ||||
|     await request | ||||
|         .post(`${base}/api/admin/archive/revive/${name}`) | ||||
|         .set('Content-Type', 'application/json'); | ||||
| 
 | ||||
|     const events = await eventStore.getEvents(); | ||||
|     t.is(events.length, 3); | ||||
|  | ||||
| @ -110,6 +110,20 @@ test('should be allowed to use new toggle name', t => { | ||||
|         .expect(200); | ||||
| }); | ||||
| 
 | ||||
| test('should get unsupported media-type when posting as form-url-encoded', t => { | ||||
|     t.plan(0); | ||||
|     const { request, base, perms } = getSetup(); | ||||
|     perms.withPermissions(CREATE_FEATURE); | ||||
| 
 | ||||
|     return request | ||||
|         .post(`${base}/api/admin/features`) | ||||
|         .type('form') | ||||
|         .send({ name: 'new.name' }) | ||||
|         .send({ enabled: true }) | ||||
|         .send({ strategies: [{ name: 'default' }] }) | ||||
|         .expect(415); | ||||
| }); | ||||
| 
 | ||||
| test('should be allowed to have variants="null"', t => { | ||||
|     t.plan(0); | ||||
|     const { request, base, perms } = getSetup(); | ||||
| @ -185,6 +199,23 @@ test('should require at least one strategy when updating a feature toggle', t => | ||||
|         .expect(400); | ||||
| }); | ||||
| 
 | ||||
| test('updating a feature toggle also requires application/json as content-type', t => { | ||||
|     t.plan(0); | ||||
|     const { request, featureToggleStore, base, perms } = getSetup(); | ||||
|     perms.withPermissions(UPDATE_FEATURE); | ||||
|     featureToggleStore.createFeature({ | ||||
|         name: 'ts', | ||||
|         strategies: [{ name: 'default' }], | ||||
|     }); | ||||
| 
 | ||||
|     return request | ||||
|         .put(`${base}/api/admin/features/ts`) | ||||
|         .type('form') | ||||
|         .send({ name: 'ts' }) | ||||
|         .send({ strategies: [{ name: 'default' }] }) | ||||
|         .expect(415); | ||||
| }); | ||||
| 
 | ||||
| test('valid feature names should pass validation', t => { | ||||
|     t.plan(0); | ||||
|     const { request, base, perms } = getSetup(); | ||||
| @ -320,6 +351,7 @@ test('should toggle on', t => { | ||||
| 
 | ||||
|     return request | ||||
|         .post(`${base}/api/admin/features/toggle.disabled/toggle/on`) | ||||
|         .set('Content-Type', 'application/json') | ||||
|         .expect('Content-Type', /json/) | ||||
|         .expect(200) | ||||
|         .expect(res => { | ||||
| @ -340,6 +372,7 @@ test('should toggle off', t => { | ||||
| 
 | ||||
|     return request | ||||
|         .post(`${base}/api/admin/features/toggle.enabled/toggle/off`) | ||||
|         .set('Content-Type', 'application/json') | ||||
|         .expect('Content-Type', /json/) | ||||
|         .expect(200) | ||||
|         .expect(res => { | ||||
| @ -360,6 +393,7 @@ test('should toggle', t => { | ||||
| 
 | ||||
|     return request | ||||
|         .post(`${base}/api/admin/features/toggle.disabled/toggle`) | ||||
|         .set('Content-Type', 'application/json') | ||||
|         .expect('Content-Type', /json/) | ||||
|         .expect(200) | ||||
|         .expect(res => { | ||||
|  | ||||
| @ -206,6 +206,7 @@ test('deprecating a strategy works', async t => { | ||||
| 
 | ||||
|     await request | ||||
|         .post(`${base}/api/admin/strategies/${name}/deprecate`) | ||||
|         .set('Content-Type', 'application/json') | ||||
|         .send() | ||||
|         .expect(200); | ||||
|     return request | ||||
| @ -220,6 +221,7 @@ test('deprecating a non-existent strategy yields 404', t => { | ||||
|     perms.withPermissions(UPDATE_STRATEGY); | ||||
|     return request | ||||
|         .post(`${base}/api/admin/strategies/non-existent-strategy/deprecate`) | ||||
|         .set('Content-Type', 'application/json') | ||||
|         .expect(404); | ||||
| }); | ||||
| 
 | ||||
| @ -232,6 +234,7 @@ test('reactivating a strategy works', async t => { | ||||
| 
 | ||||
|     await request | ||||
|         .post(`${base}/api/admin/strategies/${name}/reactivate`) | ||||
|         .set('Content-Type', 'application/json') | ||||
|         .send() | ||||
|         .expect(200); | ||||
|     return request | ||||
| @ -246,6 +249,7 @@ test('reactivating a non-existent strategy yields 404', t => { | ||||
|     perms.withPermissions(UPDATE_STRATEGY); | ||||
|     return request | ||||
|         .post(`${base}/api/admin/strategies/non-existent-strategy/reactivate`) | ||||
|         .set('Content-Type', 'application/json') | ||||
|         .expect(404); | ||||
| }); | ||||
| test(`deprecating 'default' strategy will yield 403`, t => { | ||||
| @ -254,5 +258,6 @@ test(`deprecating 'default' strategy will yield 403`, t => { | ||||
|     perms.withPermissions(UPDATE_STRATEGY); | ||||
|     return request | ||||
|         .post(`${base}/api/admin/strategies/default/deprecate`) | ||||
|         .set('Content-Type', 'application/json') | ||||
|         .expect(403); | ||||
| }); | ||||
|  | ||||
| @ -65,7 +65,10 @@ test('should register client without sdkVersin', t => { | ||||
| test('should require appName field', t => { | ||||
|     t.plan(0); | ||||
|     const { request } = getSetup(); | ||||
|     return request.post('/api/client/register').expect(400); | ||||
|     return request | ||||
|         .post('/api/client/register') | ||||
|         .set('Content-Type', 'application/json') | ||||
|         .expect(400); | ||||
| }); | ||||
| 
 | ||||
| test('should require strategies field', t => { | ||||
|  | ||||
| @ -2,6 +2,7 @@ | ||||
| 
 | ||||
| const { Router } = require('express'); | ||||
| const checkPermission = require('../middleware/permission-checker'); | ||||
| const requireContentType = require('../middleware/content_type_checker'); | ||||
| /** | ||||
|  * Base class for Controllers to standardize binding to express Router. | ||||
|  */ | ||||
| @ -20,18 +21,20 @@ class Controller { | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     post(path, handler, permission) { | ||||
|     post(path, handler, permission, ...acceptedContentTypes) { | ||||
|         this.app.post( | ||||
|             path, | ||||
|             checkPermission(this.config, permission), | ||||
|             requireContentType(...acceptedContentTypes), | ||||
|             handler.bind(this), | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     put(path, handler, permission) { | ||||
|     put(path, handler, permission, ...acceptedContentTypes) { | ||||
|         this.app.put( | ||||
|             path, | ||||
|             checkPermission(this.config, permission), | ||||
|             requireContentType(...acceptedContentTypes), | ||||
|             handler.bind(this), | ||||
|         ); | ||||
|     } | ||||
|  | ||||
| @ -128,12 +128,10 @@ | ||||
|   }, | ||||
|   "lint-staged": { | ||||
|     "*.js": [ | ||||
|       "eslint --fix", | ||||
|       "git add" | ||||
|       "eslint --fix" | ||||
|     ], | ||||
|     "*.{json,css,md}": [ | ||||
|       "prettier --write", | ||||
|       "git add" | ||||
|       "prettier --write" | ||||
|     ] | ||||
|   }, | ||||
|   "husky": { | ||||
|  | ||||
| @ -130,6 +130,7 @@ test.serial('deprecating a strategy works', async t => { | ||||
|         .expect(201); | ||||
|     await request | ||||
|         .post(`/api/admin/strategies/${name}/deprecate`) | ||||
|         .set('Content-Type', 'application/json') | ||||
|         .send() | ||||
|         .expect(200); | ||||
|     await request | ||||
| @ -149,6 +150,7 @@ test.serial('can reactivate a deprecated strategy', async t => { | ||||
|         .expect(201); | ||||
|     await request | ||||
|         .post(`/api/admin/strategies/${name}/deprecate`) | ||||
|         .set('Content-Type', 'application/json') | ||||
|         .send() | ||||
|         .expect(200); | ||||
|     await request | ||||
| @ -158,6 +160,7 @@ test.serial('can reactivate a deprecated strategy', async t => { | ||||
|         .expect(res => t.is(res.body.deprecated, true)); | ||||
|     await request | ||||
|         .post(`/api/admin/strategies/${name}/reactivate`) | ||||
|         .set('Content-Type', 'application/json') | ||||
|         .send() | ||||
|         .expect(200); | ||||
|     await request | ||||
| @ -170,7 +173,10 @@ test.serial('can reactivate a deprecated strategy', async t => { | ||||
| test.serial('cannot deprecate default strategy', async t => { | ||||
|     t.plan(0); | ||||
|     const request = await setupApp(stores); | ||||
|     await request.post('/api/admin/strategies/default/deprecate').expect(403); | ||||
|     await request | ||||
|         .post('/api/admin/strategies/default/deprecate') | ||||
|         .set('Content-Type', 'application/json') | ||||
|         .expect(403); | ||||
| }); | ||||
| 
 | ||||
| test.serial('can update a exiting strategy with deprecated', async t => { | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user