mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	feat: authorization middleware (#3464)
This commit is contained in:
		
							parent
							
								
									70d802883b
								
							
						
					
					
						commit
						33487d11b9
					
				| @ -123,13 +123,15 @@ export const UserProfileContent = ({ | ||||
| 
 | ||||
|                 <StyledDivider /> | ||||
| 
 | ||||
|                 <StyledLogoutButton | ||||
|                     variant="outlined" | ||||
|                     color="primary" | ||||
|                     href={`${basePath}/logout`} | ||||
|                 > | ||||
|                     Log out | ||||
|                 </StyledLogoutButton> | ||||
|                 <form method="POST" action={`${basePath}/logout`}> | ||||
|                     <StyledLogoutButton | ||||
|                         type="submit" | ||||
|                         variant="outlined" | ||||
|                         color="primary" | ||||
|                     > | ||||
|                         Log out | ||||
|                     </StyledLogoutButton> | ||||
|                 </form> | ||||
|             </StyledPaper> | ||||
|         } | ||||
|     /> | ||||
|  | ||||
| @ -107,7 +107,7 @@ export default async function getApp( | ||||
|     switch (config.authentication.type) { | ||||
|         case IAuthType.OPEN_SOURCE: { | ||||
|             app.use(baseUriPath, apiTokenMiddleware(config, services)); | ||||
|             ossAuthentication(app, config.server.baseUriPath); | ||||
|             ossAuthentication(app, config.getLogger, config.server.baseUriPath); | ||||
|             break; | ||||
|         } | ||||
|         case IAuthType.ENTERPRISE: { | ||||
|  | ||||
							
								
								
									
										39
									
								
								src/lib/middleware/authorization-middleware.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								src/lib/middleware/authorization-middleware.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,39 @@ | ||||
| import { IAuthRequest } from '../routes/unleash-types'; | ||||
| import { NextFunction, Response } from 'express'; | ||||
| import AuthenticationRequired from '../types/authentication-required'; | ||||
| import { LogProvider } from '../logger'; | ||||
| 
 | ||||
| /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ | ||||
| const authorizationMiddleware = ( | ||||
|     getLogger: LogProvider, | ||||
|     baseUriPath: string, | ||||
| ): any => { | ||||
|     const logger = getLogger('/middleware/authorization-middleware.ts'); | ||||
|     logger.debug('Enabling Authorization middleware'); | ||||
| 
 | ||||
|     const generateAuthResponse = async () => | ||||
|         new AuthenticationRequired({ | ||||
|             type: 'password', | ||||
|             path: `${baseUriPath}/auth/simple/login`, | ||||
|             message: 'You must sign in order to use Unleash', | ||||
|         }); | ||||
| 
 | ||||
|     return async (req: IAuthRequest, res: Response, next: NextFunction) => { | ||||
|         if (req.session && req.session.user) { | ||||
|             req.user = req.session.user; | ||||
|             return next(); | ||||
|         } | ||||
|         if (req.user) { | ||||
|             return next(); | ||||
|         } | ||||
|         if (req.header('authorization')) { | ||||
|             // API clients should get 401 without body
 | ||||
|             return res.sendStatus(401); | ||||
|         } | ||||
|         // Admin UI users should get auth-response
 | ||||
|         const authRequired = await generateAuthResponse(); | ||||
|         return res.status(401).json(authRequired); | ||||
|     }; | ||||
| }; | ||||
| 
 | ||||
| export default authorizationMiddleware; | ||||
| @ -7,6 +7,10 @@ import ossAuth from './oss-authentication'; | ||||
| import getApp from '../app'; | ||||
| import User from '../types/user'; | ||||
| import sessionDb from './session-db'; | ||||
| import { Knex } from 'knex'; | ||||
| import { LogProvider } from '../logger'; | ||||
| 
 | ||||
| const getLogger = (() => ({ debug() {} })) as unknown as LogProvider; | ||||
| 
 | ||||
| async function getSetup(preRouterHook) { | ||||
|     const base = `/random${Math.round(Math.random() * 1000)}`; | ||||
| @ -14,7 +18,7 @@ async function getSetup(preRouterHook) { | ||||
|         server: { baseUriPath: base }, | ||||
|         preRouterHook: (_app) => { | ||||
|             preRouterHook(_app); | ||||
|             ossAuth(_app, base); | ||||
|             ossAuth(_app, getLogger, base); | ||||
|             _app.get(`${base}/api/protectedResource`, (req, res) => { | ||||
|                 res.status(200).json({ message: 'OK' }).end(); | ||||
|             }); | ||||
| @ -22,7 +26,7 @@ async function getSetup(preRouterHook) { | ||||
|     }); | ||||
|     const stores = createStores(); | ||||
|     const services = createServices(stores, config); | ||||
|     const unleashSession = sessionDb(config, undefined); | ||||
|     const unleashSession = sessionDb(config, {} as Knex); | ||||
|     const app = await getApp(config, stores, services, unleashSession); | ||||
| 
 | ||||
|     return { | ||||
|  | ||||
| @ -1,33 +1,19 @@ | ||||
| import { Application, NextFunction, Response } from 'express'; | ||||
| import { IAuthRequest } from '../routes/unleash-types'; | ||||
| import AuthenticationRequired from '../types/authentication-required'; | ||||
| 
 | ||||
| function ossAuthHook(app: Application, baseUriPath: string): void { | ||||
|     const generateAuthResponse = async () => | ||||
|         new AuthenticationRequired({ | ||||
|             type: 'password', | ||||
|             path: `${baseUriPath}/auth/simple/login`, | ||||
|             message: 'You must sign in order to use Unleash', | ||||
|         }); | ||||
| import { Application } from 'express'; | ||||
| import authorizationMiddleware from './authorization-middleware'; | ||||
| import { LogProvider } from '../logger'; | ||||
| 
 | ||||
| function ossAuthHook( | ||||
|     app: Application, | ||||
|     getLogger: LogProvider, | ||||
|     baseUriPath: string, | ||||
| ): void { | ||||
|     app.use( | ||||
|         `${baseUriPath}/api`, | ||||
|         async (req: IAuthRequest, res: Response, next: NextFunction) => { | ||||
|             if (req.session && req.session.user) { | ||||
|                 req.user = req.session.user; | ||||
|                 return next(); | ||||
|             } | ||||
|             if (req.user) { | ||||
|                 return next(); | ||||
|             } | ||||
|             if (req.header('authorization')) { | ||||
|                 // API clients should get 401 without body
 | ||||
|                 return res.sendStatus(401); | ||||
|             } | ||||
|             // Admin UI users should get auth-response
 | ||||
|             const authRequired = await generateAuthResponse(); | ||||
|             return res.status(401).json(authRequired); | ||||
|         }, | ||||
|         authorizationMiddleware(getLogger, baseUriPath), | ||||
|     ); | ||||
|     app.use( | ||||
|         `${baseUriPath}/logout`, | ||||
|         authorizationMiddleware(getLogger, baseUriPath), | ||||
|     ); | ||||
| } | ||||
| export default ossAuthHook; | ||||
|  | ||||
| @ -37,6 +37,7 @@ function sessionDb( | ||||
|                     : config.server.baseUriPath, | ||||
|             secure: config.secureHeaders, | ||||
|             maxAge: age, | ||||
|             sameSite: 'lax', | ||||
|         }, | ||||
|     }); | ||||
| } | ||||
|  | ||||
| @ -27,7 +27,7 @@ test('should redirect to "/" after logout', async () => { | ||||
|     const request = supertest(app); | ||||
|     expect.assertions(0); | ||||
|     await request | ||||
|         .get(`${baseUriPath}/logout`) | ||||
|         .post(`${baseUriPath}/logout`) | ||||
|         .expect(302) | ||||
|         .expect('Location', `${baseUriPath}/`); | ||||
| }); | ||||
| @ -45,7 +45,7 @@ test('should redirect to "/basePath" after logout when baseUriPath is set', asyn | ||||
|     const request = supertest(app); | ||||
|     expect.assertions(0); | ||||
|     await request | ||||
|         .get('/logout') | ||||
|         .post('/logout') | ||||
|         .expect(302) | ||||
|         .expect('Location', `${baseUriPath}/`); | ||||
| }); | ||||
| @ -64,7 +64,7 @@ test('should set "Clear-Site-Data" header', async () => { | ||||
|     const request = supertest(app); | ||||
|     expect.assertions(0); | ||||
|     await request | ||||
|         .get(`${baseUriPath}/logout`) | ||||
|         .post(`${baseUriPath}/logout`) | ||||
|         .expect(302) | ||||
|         .expect('Clear-Site-Data', '"cookies", "storage"'); | ||||
| }); | ||||
| @ -86,7 +86,7 @@ test('should not set "Clear-Site-Data" header', async () => { | ||||
|     const request = supertest(app); | ||||
|     expect.assertions(1); | ||||
|     await request | ||||
|         .get(`${baseUriPath}/logout`) | ||||
|         .post(`${baseUriPath}/logout`) | ||||
|         .expect(302) | ||||
|         .expect((res) => | ||||
|             expect(res.headers['Clear-Site-Data']).toBeUndefined(), | ||||
| @ -108,7 +108,7 @@ test('should clear "unleash-session" cookies', async () => { | ||||
|     const request = supertest(app); | ||||
|     expect.assertions(0); | ||||
|     await request | ||||
|         .get(`${baseUriPath}/logout`) | ||||
|         .post(`${baseUriPath}/logout`) | ||||
|         .expect(302) | ||||
|         .expect( | ||||
|             'Set-Cookie', | ||||
| @ -134,7 +134,7 @@ test('should clear "unleash-session" cookie even when disabled clear site data', | ||||
|     const request = supertest(app); | ||||
|     expect.assertions(0); | ||||
|     await request | ||||
|         .get(`${baseUriPath}/logout`) | ||||
|         .post(`${baseUriPath}/logout`) | ||||
|         .expect(302) | ||||
|         .expect( | ||||
|             'Set-Cookie', | ||||
| @ -162,7 +162,7 @@ test('should call destroy on session', async () => { | ||||
|     app.use('/logout', new LogoutController(config, { sessionService }).router); | ||||
| 
 | ||||
|     const request = supertest(app); | ||||
|     await request.get(`${baseUriPath}/logout`); | ||||
|     await request.post(`${baseUriPath}/logout`); | ||||
| 
 | ||||
|     expect(fakeSession.destroy.mock.calls.length).toBe(1); | ||||
| }); | ||||
| @ -186,7 +186,7 @@ test('should handle req.logout with callback function', async () => { | ||||
|     app.use('/logout', new LogoutController(config, { sessionService }).router); | ||||
| 
 | ||||
|     const request = supertest(app); | ||||
|     await request.get(`${baseUriPath}/logout`); | ||||
|     await request.post(`${baseUriPath}/logout`); | ||||
| 
 | ||||
|     expect(logoutFunction).toHaveBeenCalledTimes(1); | ||||
|     expect(logoutFunction).toHaveBeenCalledWith(expect.anything()); | ||||
| @ -211,7 +211,7 @@ test('should handle req.logout without callback function', async () => { | ||||
|     app.use('/logout', new LogoutController(config, { sessionService }).router); | ||||
| 
 | ||||
|     const request = supertest(app); | ||||
|     await request.get(`${baseUriPath}/logout`); | ||||
|     await request.post(`${baseUriPath}/logout`); | ||||
| 
 | ||||
|     expect(logoutFunction).toHaveBeenCalledTimes(1); | ||||
|     expect(logoutFunction).toHaveBeenCalledWith(); | ||||
| @ -238,7 +238,7 @@ test('should redirect to alternative logoutUrl', async () => { | ||||
| 
 | ||||
|     const request = supertest(app); | ||||
|     await request | ||||
|         .get('/logout') | ||||
|         .post('/logout') | ||||
|         .expect(302) | ||||
|         .expect('Location', '/some-other-path'); | ||||
| }); | ||||
| @ -282,7 +282,7 @@ test('Should destroy sessions for user', async () => { | ||||
|     let activeSessionsBeforeLogout = await sessionStore.getSessionsForUser(1); | ||||
|     expect(activeSessionsBeforeLogout).toHaveLength(2); | ||||
|     app.use('/logout', new LogoutController(config, { sessionService }).router); | ||||
|     await supertest(app).get('/logout').expect(302); | ||||
|     await supertest(app).post('/logout').expect(302); | ||||
|     let activeSessions = await sessionStore.getSessionsForUser(1); | ||||
|     expect(activeSessions).toHaveLength(0); | ||||
| }); | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| import { Response } from 'express'; | ||||
| import { promisify } from 'util'; | ||||
| import { IUnleashConfig } from '../types'; | ||||
| import { IUnleashConfig, NONE } from '../types'; | ||||
| import Controller from './controller'; | ||||
| import { IAuthRequest } from './unleash-types'; | ||||
| import { IUnleashServices } from '../types'; | ||||
| @ -24,7 +24,14 @@ class LogoutController extends Controller { | ||||
|         this.baseUri = config.server.baseUriPath; | ||||
|         this.clearSiteDataOnLogout = config.session.clearSiteDataOnLogout; | ||||
|         this.cookieName = config.session.cookieName; | ||||
|         this.get('/', this.logout); | ||||
| 
 | ||||
|         this.route({ | ||||
|             method: 'post', | ||||
|             path: '/', | ||||
|             handler: this.logout, | ||||
|             permission: NONE, | ||||
|             acceptAnyContentType: true, | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     async logout(req: IAuthRequest, res: Response): Promise<void> { | ||||
|  | ||||
| @ -222,7 +222,7 @@ test('should not fail creation of PAT when a description already exists for anot | ||||
| }); | ||||
| 
 | ||||
| test('should get user id 1', async () => { | ||||
|     await app.request.get('/logout').expect(302); | ||||
|     await app.request.post('/logout').expect(302); | ||||
|     await app.request | ||||
|         .get('/api/admin/user') | ||||
|         .set('Authorization', firstSecret) | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user