From 641b27101f5a8ab67bdde960b96668347afcc4ca Mon Sep 17 00:00:00 2001 From: Abolaji Oyerinde Date: Sat, 3 Jun 2023 22:07:01 +0100 Subject: [PATCH 1/5] test: added unit test for file CommonServer/Middleware/UserAuthorization --- .../Middleware/UserAuthorization.test.ts | 594 ++++++++++++++++++ 1 file changed, 594 insertions(+) create mode 100644 CommonServer/Tests/Middleware/UserAuthorization.test.ts diff --git a/CommonServer/Tests/Middleware/UserAuthorization.test.ts b/CommonServer/Tests/Middleware/UserAuthorization.test.ts new file mode 100644 index 0000000000..1522d363f0 --- /dev/null +++ b/CommonServer/Tests/Middleware/UserAuthorization.test.ts @@ -0,0 +1,594 @@ +import Dictionary from 'Common/Types/Dictionary'; +import UserMiddleware from '../../Middleware/UserAuthorization'; +import { + ExpressRequest, + ExpressResponse, + NextFunction, +} from '../../Utils/Express'; +import JSONWebToken from '../../Utils/JsonWebToken'; +import logger from '../../Utils/Logger'; +import JSONWebTokenData from 'Common/Types/JsonWebTokenData'; +import ObjectID from 'Common/Types/ObjectID'; +import ProjectMiddleware from '../../Middleware/ProjectAuthorization'; +import UserService from '../../Services/UserService'; +import Email from 'Common/Types/Email'; +import ProjectService from '../../Services/ProjectService'; +import Response from '../../Utils/Response'; +import BadDataException from 'Common/Types/Exception/BadDataException'; +import Project from 'Model/Models/Project'; +import SsoAuthorizationException from 'Common/Types/Exception/SsoAuthorizationException'; +import AccessTokenService from '../../Services/AccessTokenService'; +import { + UserGlobalAccessPermission, + UserTenantAccessPermission, +} from 'Common/Types/Permission'; +import JSONFunctions from 'Common/Types/JSONFunctions'; +import HashedString from 'Common/Types/HashedString'; + +jest.mock('../../Utils/Logger'); +jest.mock('../../Middleware/ProjectAuthorization'); +jest.mock('../../Utils/JsonWebToken'); +jest.mock('../../Services/UserService'); +jest.mock('../../Services/AccessTokenService'); +jest.mock('../../Utils/Response'); +jest.mock('../../Services/ProjectService'); +jest.mock('Common/Types/HashedString'); + +type StringOrNull = string | null; + +describe('UserMiddleware', () => { + const mockedAccessToken: string = 'token'; + const projectId: ObjectID = ObjectID.generate(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('getAccessToken', () => { + test('should return access token when authorization token is passed in the request header', () => { + const req: ExpressRequest = { + headers: { authorization: mockedAccessToken }, + query: {}, + } as ExpressRequest; + + const result: StringOrNull = UserMiddleware.getAccessToken(req); + + expect(result).toEqual(mockedAccessToken); + }); + + test('should return access token when accessToken token is passed in the request query', () => { + const req: Partial = { + query: { accessToken: mockedAccessToken }, + headers: {}, + }; + + const result: StringOrNull = UserMiddleware.getAccessToken( + req as ExpressRequest + ); + + expect(result).toEqual(mockedAccessToken); + }); + + test('should split and return the access token part of a bearer authorization token', () => { + const req: ExpressRequest = { + headers: { authorization: `Bearer ${mockedAccessToken}` }, + query: {}, + } as ExpressRequest; + + const result: StringOrNull = UserMiddleware.getAccessToken(req); + + expect(result).toEqual(mockedAccessToken); + }); + + test('should return null when authorization nor accessToken is passed', () => { + const req: ExpressRequest = { + headers: {}, + query: {}, + } as ExpressRequest; + + const result: StringOrNull = UserMiddleware.getAccessToken(req); + + expect(result).toBeNull(); + }); + }); + + describe('getSsoTokens', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + const req: Partial = { + headers: { 'sso-token': mockedAccessToken }, + }; + + test('should return an empty object when ssoToken is not passed', () => { + const result: Dictionary = UserMiddleware.getSsoTokens({ + headers: {}, + } as ExpressRequest); + + expect(result).toEqual({}); + }); + + test('should return an empty object when ssoToken cannot be decoded', () => { + const error: Error = new Error('Invalid token'); + const spyDecode: jest.SpyInstance = jest + .spyOn(JSONWebToken, 'decode') + .mockImplementationOnce((_: string) => { + throw error; + }); + const spyErrorLogger: jest.SpyInstance = jest.spyOn( + logger, + 'error' + ); + + const result: Dictionary = UserMiddleware.getSsoTokens( + req as ExpressRequest + ); + + expect(result).toEqual({}); + expect(spyDecode).toHaveBeenCalledWith(mockedAccessToken); + expect(spyErrorLogger).toHaveBeenCalledWith(error); + }); + + test("should return an empty object when the decoded sso-token object doesn't have projectId property", () => { + const spyDecode: jest.SpyInstance = jest + .spyOn(JSONWebToken, 'decode') + .mockReturnValueOnce({} as JSONWebTokenData); + const spyErrorLogger: jest.SpyInstance = jest.spyOn( + logger, + 'error' + ); + + const result: Dictionary = UserMiddleware.getSsoTokens( + req as ExpressRequest + ); + + expect(result).toEqual({}); + expect(spyDecode).toHaveBeenCalledWith(mockedAccessToken); + expect(spyErrorLogger).not.toBeCalled(); + }); + + test('should return a dictionary of string with projectId key', () => { + jest.spyOn(JSONWebToken, 'decode').mockReturnValueOnce({ + projectId, + } as JSONWebTokenData); + + const result: Dictionary = UserMiddleware.getSsoTokens( + req as ExpressRequest + ); + + expect(result).toEqual({ + [projectId.toString()]: mockedAccessToken, + }); + }); + }); + + describe('doesSsoTokenForProjectExist', () => { + const req: ExpressRequest = {} as ExpressRequest; + const userId: ObjectID = ObjectID.generate(); + + beforeAll(() => { + jest.spyOn(UserMiddleware, 'getSsoTokens').mockReturnValue({ + [projectId.toString()]: mockedAccessToken, + }); + }); + + test('should return false, when getSsoTokens does not return a value', () => { + const spyGetSsoTokens: jest.SpyInstance = jest + .spyOn(UserMiddleware, 'getSsoTokens') + .mockImplementationOnce(jest.fn().mockReturnValue(null)); + + const result: boolean = UserMiddleware.doesSsoTokenForProjectExist( + req, + projectId, + userId + ); + + expect(result).toStrictEqual(false); + expect(spyGetSsoTokens).toHaveBeenCalled(); + }); + + test("should return false, when getSsoTokens returns a dictionary that does not contain the projectId's value as key", () => { + const spyGetSsoTokens: jest.SpyInstance = jest + .spyOn(UserMiddleware, 'getSsoTokens') + .mockReturnValueOnce({}); + + const result: boolean = UserMiddleware.doesSsoTokenForProjectExist( + req, + projectId, + userId + ); + + expect(result).toStrictEqual(false); + expect(spyGetSsoTokens).toHaveBeenCalledWith(req); + }); + + test("should return false, when decoded JWT object's projectId value does not match with projectId passed as parameter", () => { + const objectId: ObjectID = ObjectID.generate(); + + const spyDecode: jest.SpyInstance = jest + .spyOn(JSONWebToken, 'decode') + .mockReturnValueOnce({ + projectId: objectId, + userId, + } as JSONWebTokenData); + + const result: boolean = UserMiddleware.doesSsoTokenForProjectExist( + req, + projectId, + userId + ); + + expect(result).toStrictEqual(false); + expect(spyDecode).toHaveBeenCalledWith(mockedAccessToken); + }); + + test("should return false, when decoded JWT object's userId does not match with userId passed as parameter", () => { + const objectId: ObjectID = ObjectID.generate(); + + const spyDecode: jest.SpyInstance = jest + .spyOn(JSONWebToken, 'decode') + .mockReturnValueOnce({ + userId: objectId, + projectId, + } as JSONWebTokenData); + + const result: boolean = UserMiddleware.doesSsoTokenForProjectExist( + req, + projectId, + userId + ); + + expect(result).toStrictEqual(false); + expect(spyDecode).toHaveBeenCalledWith(mockedAccessToken); + }); + + test('should return true', () => { + jest.spyOn(JSONWebToken, 'decode').mockReturnValueOnce({ + userId, + projectId, + } as JSONWebTokenData); + + const result: boolean = UserMiddleware.doesSsoTokenForProjectExist( + req, + projectId, + userId + ); + + expect(result).toStrictEqual(true); + }); + }); + + describe('getUserMiddleware', () => { + let req: ExpressRequest; + let res: ExpressResponse; + + const next: NextFunction = jest.fn(); + + const mockedUserId: ObjectID = ObjectID.generate(); + const hashValue: string = 'hash-value'; + + const jwtTokenData: JSONWebTokenData = { + userId: mockedUserId, + isMasterAdmin: true, + email: new Email('test@gmail.com'), + }; + + beforeAll(() => { + jest.spyOn(ProjectMiddleware, 'getProjectId').mockReturnValue( + projectId + ); + jest.spyOn(ProjectMiddleware, 'hasApiKey').mockReturnValue(false); + jest.spyOn(UserMiddleware, 'getAccessToken').mockReturnValue( + mockedAccessToken + ); + jest.spyOn(JSONWebToken, 'decode').mockReturnValue(jwtTokenData); + jest.spyOn(HashedString, 'hashValue').mockResolvedValue(hashValue); + }); + + beforeEach(() => { + req = { headers: {} } as ExpressRequest; + + res = {} as ExpressResponse; + res.set = jest.fn(); + }); + + test('should call isValidProjectIdAndApiKeyMiddleware and return when hasApiKey returns true', async () => { + jest.spyOn(ProjectMiddleware, 'hasApiKey').mockReturnValueOnce( + true + ); + + const spyGetAccessToken: jest.SpyInstance = jest.spyOn( + UserMiddleware, + 'getAccessToken' + ); + + await UserMiddleware.getUserMiddleware(req, res, next); + + expect( + ProjectMiddleware.isValidProjectIdAndApiKeyMiddleware + ).toHaveBeenCalledWith(req, res, next); + expect(spyGetAccessToken).not.toHaveBeenCalled(); + }); + + test("should call function 'next' and return, when getAccessToken returns a string value", async () => { + const spyGetAccessToken: jest.SpyInstance = jest + .spyOn(UserMiddleware, 'getAccessToken') + .mockReturnValueOnce(null); + + await UserMiddleware.getUserMiddleware(req, res, next); + + expect(next).toHaveBeenCalled(); + expect(spyGetAccessToken).toHaveBeenCalledWith(req); + expect(JSONWebToken.decode).not.toHaveBeenCalled(); + }); + + test("should call function 'next' and return, when accessToken can not be decoded", async () => { + const error: Error = new Error('Invalid access token'); + + const spyJWTDecode: jest.SpyInstance = jest + .spyOn(JSONWebToken, 'decode') + .mockImplementationOnce((_: string) => { + throw error; + }); + + await UserMiddleware.getUserMiddleware(req, res, next); + expect(next).toHaveBeenCalled(); + expect(spyJWTDecode).toHaveBeenCalledWith(mockedAccessToken); + expect(UserService.updateOneBy).not.toHaveBeenCalled(); + }); + + test('should set global-permissions and global-permissions-hash in the response header, when user has global access permission', async () => { + const mockedGlobalAccessPermission: UserGlobalAccessPermission = + {} as UserGlobalAccessPermission; + + jest.spyOn(ProjectMiddleware, 'getProjectId').mockReturnValueOnce( + null + ); + const spySerialize: jest.SpyInstance = jest + .spyOn(JSONFunctions, 'serialize') + .mockReturnValueOnce({}); + const spyGetUserGlobalAccessPermission: jest.SpyInstance = jest + .spyOn(AccessTokenService, 'getUserGlobalAccessPermission') + .mockResolvedValueOnce(mockedGlobalAccessPermission); + + await UserMiddleware.getUserMiddleware(req, res, next); + + expect(spyGetUserGlobalAccessPermission).toHaveBeenCalledWith( + jwtTokenData.userId + ); + expect(spySerialize).toHaveBeenCalledWith( + mockedGlobalAccessPermission + ); + expect(res.set).toHaveBeenCalledWith( + 'global-permissions', + JSON.stringify({}) + ); + expect(res.set).toHaveBeenCalledWith( + 'global-permissions-hash', + hashValue + ); + expect(next).toHaveBeenCalled(); + }); + + test('should not set global-permissions and global-permissions-hash in the response header, when user does not have global access permission', async () => { + jest.spyOn(ProjectMiddleware, 'getProjectId').mockReturnValueOnce( + null + ); + const spyGetUserGlobalAccessPermission: jest.SpyInstance = jest + .spyOn(AccessTokenService, 'getUserGlobalAccessPermission') + .mockResolvedValueOnce(null); + + await UserMiddleware.getUserMiddleware(req, res, next); + + expect(spyGetUserGlobalAccessPermission).toHaveBeenCalledWith( + jwtTokenData.userId + ); + expect(res.set).not.toHaveBeenCalledWith( + 'global-permissions', + expect.anything() + ); + expect(res.set).not.toHaveBeenCalledWith( + 'global-permissions-hash', + expect.anything() + ); + expect(next).toHaveBeenCalled(); + }); + + test('should return Invalid tenantId error, when tenantId is not null and project is not found', async () => { + const spyFindOneById: jest.SpyInstance = jest + .spyOn(ProjectService, 'findOneById') + .mockResolvedValueOnce(null); + + await UserMiddleware.getUserMiddleware(req, res, next); + + expect(Response.sendErrorResponse).toHaveBeenCalledWith( + req, + res, + new BadDataException('Invalid tenantId') + ); + expect(spyFindOneById).toHaveBeenCalledWith({ + id: projectId, + select: { + requireSsoForLogin: true, + }, + props: { + isRoot: true, + }, + }); + }); + + test('should return SSO Authorization Required error, when sso is required for login but sso token for project does not exist', async () => { + jest.spyOn(ProjectService, 'findOneById').mockResolvedValueOnce({ + requireSsoForLogin: true, + } as Project); + + const spyDoesSsoTokenForProjectExist: jest.SpyInstance = jest + .spyOn(UserMiddleware, 'doesSsoTokenForProjectExist') + .mockReturnValueOnce(false); + + await UserMiddleware.getUserMiddleware(req, res, next); + + expect(Response.sendErrorResponse).toHaveBeenCalledWith( + req, + res, + new SsoAuthorizationException() + ); + expect(spyDoesSsoTokenForProjectExist).toHaveBeenCalledWith( + req, + projectId, + jwtTokenData.userId + ); + }); + + test('should set project-permissions and project-permissions-hash in the response header when tenantId is not null and user has tenant access permission', async () => { + const mockedTenantAccessPermission: UserTenantAccessPermission = + {} as UserTenantAccessPermission; + + jest.spyOn(ProjectService, 'findOneById').mockResolvedValueOnce({ + requireSsoForLogin: false, + } as Project); + const spySerialize: jest.SpyInstance = jest + .spyOn(JSONFunctions, 'serialize') + .mockReturnValueOnce({}); + const spyGetUserTenantAccessPermission: jest.SpyInstance = jest + .spyOn(AccessTokenService, 'getUserTenantAccessPermission') + .mockResolvedValueOnce(mockedTenantAccessPermission); + + await UserMiddleware.getUserMiddleware(req, res, next); + + expect(spyGetUserTenantAccessPermission).toHaveBeenCalledWith( + jwtTokenData.userId, + projectId + ); + expect(spySerialize).toHaveBeenCalledWith( + mockedTenantAccessPermission + ); + expect(res.set).toHaveBeenCalledWith( + 'project-permissions', + JSON.stringify({}) + ); + expect(res.set).toHaveBeenCalledWith( + 'project-permissions-hash', + hashValue + ); + expect(next).toHaveBeenCalled(); + }); + + test('should set project-permissions and project-permissions-hash in the response header with default tenant access permission, when is-multi-tenant-query request header is set and sso-token does not exist', async () => { + const mockedTenantAccessPermission: UserTenantAccessPermission = + {} as UserTenantAccessPermission; + const mockedGlobalAccessPermission: UserGlobalAccessPermission = { + projectIds: [projectId], + } as UserGlobalAccessPermission; + const project: Project = { + _id: projectId.toString(), + requireSsoForLogin: true, + } as Project; + + const mockedRequest: Partial = { + headers: { 'is-multi-tenant-query': 'yes' }, + }; + + jest.spyOn(ProjectService, 'findBy').mockResolvedValueOnce([ + project, + ]); + jest.spyOn(ProjectService, 'findOneById').mockResolvedValueOnce({ + ...project, + requireSsoForLogin: false, + } as Project); + jest.spyOn( + UserMiddleware, + 'doesSsoTokenForProjectExist' + ).mockReturnValueOnce(false); + jest.spyOn(JSONFunctions, 'serialize').mockReturnValueOnce({}); + jest.spyOn( + AccessTokenService, + 'getUserGlobalAccessPermission' + ).mockResolvedValueOnce(mockedGlobalAccessPermission); + jest.spyOn( + AccessTokenService, + 'getUserTenantAccessPermission' + ).mockResolvedValueOnce(null); + + const spyGetDefaultUserTenantAccessPermission: jest.SpyInstance = + jest + .spyOn( + AccessTokenService, + 'getDefaultUserTenantAccessPermission' + ) + .mockReturnValueOnce(mockedTenantAccessPermission); + + await UserMiddleware.getUserMiddleware( + mockedRequest as ExpressRequest, + res, + next + ); + + expect( + spyGetDefaultUserTenantAccessPermission + ).toHaveBeenCalledWith(projectId); + expect(res.set).toHaveBeenCalledWith( + 'project-permissions', + JSON.stringify({}) + ); + expect(res.set).toHaveBeenCalledWith( + 'project-permissions-hash', + hashValue + ); + expect(next).toHaveBeenCalled(); + }); + + test('should set project-permissions and project-permissions-hash in the response header with user tenant access permission, when is-multi-tenant-query request header is set and user has tenant access permission', async () => { + const mockedTenantAccessPermission: UserTenantAccessPermission = + {} as UserTenantAccessPermission; + const mockedGlobalAccessPermission: UserGlobalAccessPermission = { + projectIds: [projectId], + } as UserGlobalAccessPermission; + const project: Project = { + _id: projectId.toString(), + requireSsoForLogin: true, + } as Project; + + const mockedRequest: Partial = { + headers: { 'is-multi-tenant-query': 'yes' }, + }; + + jest.spyOn(ProjectService, 'findBy').mockResolvedValueOnce([ + project, + ]); + jest.spyOn(ProjectService, 'findOneById').mockResolvedValueOnce({ + ...project, + requireSsoForLogin: false, + } as Project); + jest.spyOn( + UserMiddleware, + 'doesSsoTokenForProjectExist' + ).mockReturnValueOnce(true); + jest.spyOn(JSONFunctions, 'serialize').mockReturnValueOnce({}); + jest.spyOn( + AccessTokenService, + 'getUserGlobalAccessPermission' + ).mockResolvedValueOnce(mockedGlobalAccessPermission); + jest.spyOn(AccessTokenService, 'getUserTenantAccessPermission') + .mockResolvedValueOnce(null) + .mockResolvedValueOnce(mockedTenantAccessPermission); + + await UserMiddleware.getUserMiddleware( + mockedRequest as ExpressRequest, + res, + next + ); + + expect(res.set).toHaveBeenCalledWith( + 'project-permissions', + JSON.stringify({}) + ); + expect(res.set).toHaveBeenCalledWith( + 'project-permissions-hash', + hashValue + ); + expect(next).toHaveBeenCalled(); + }); + }); +}); From ad907aa854f023082f734d785c8dbec5c48cce18 Mon Sep 17 00:00:00 2001 From: Abolaji Oyerinde Date: Sat, 3 Jun 2023 22:26:14 +0100 Subject: [PATCH 2/5] refactor: changed hard-coded accessToken to ObjectID generated value --- CommonServer/Tests/Middleware/UserAuthorization.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CommonServer/Tests/Middleware/UserAuthorization.test.ts b/CommonServer/Tests/Middleware/UserAuthorization.test.ts index 1522d363f0..47a83e7763 100644 --- a/CommonServer/Tests/Middleware/UserAuthorization.test.ts +++ b/CommonServer/Tests/Middleware/UserAuthorization.test.ts @@ -37,7 +37,7 @@ jest.mock('Common/Types/HashedString'); type StringOrNull = string | null; describe('UserMiddleware', () => { - const mockedAccessToken: string = 'token'; + const mockedAccessToken: string = ObjectID.generate().toString(); const projectId: ObjectID = ObjectID.generate(); beforeEach(() => { From 0426a86f62e4aa4923a2313d33bb359c71716fa1 Mon Sep 17 00:00:00 2001 From: Abolaji Oyerinde Date: Sun, 4 Jun 2023 14:57:37 +0100 Subject: [PATCH 3/5] refactor: splitted function getUserMiddleware into sub-functions --- CommonServer/Middleware/UserAuthorization.ts | 233 ++++++++++--------- 1 file changed, 128 insertions(+), 105 deletions(-) diff --git a/CommonServer/Middleware/UserAuthorization.ts b/CommonServer/Middleware/UserAuthorization.ts index 89e352d4c8..8d74b79545 100644 --- a/CommonServer/Middleware/UserAuthorization.ts +++ b/CommonServer/Middleware/UserAuthorization.ts @@ -28,6 +28,7 @@ import BadDataException from 'Common/Types/Exception/BadDataException'; import SsoAuthorizationException from 'Common/Types/Exception/SsoAuthorizationException'; import JSONWebTokenData from 'Common/Types/JsonWebTokenData'; import logger from '../Utils/Logger'; +import Exception from 'Common/Types/Exception/Exception'; export default class UserMiddleware { /* @@ -153,6 +154,7 @@ export default class UserMiddleware { oneuptimeRequest.userType = UserType.User; } + const { userId: userObjectId } = oneuptimeRequest.userAuthorization; const userId: string = oneuptimeRequest.userAuthorization.userId.toString(); @@ -175,121 +177,40 @@ export default class UserMiddleware { } if (tenantId) { - const project: Project | null = await ProjectService.findOneById({ - id: tenantId, - select: { - requireSsoForLogin: true, - }, - props: { - isRoot: true, - }, - }); + try { + const userTenantAccessPermission: UserTenantAccessPermission | null = + await this.getUserTenantAccessPermissionWithTenantId( + req, + tenantId, + userObjectId + ); - if (!project) { - return Response.sendErrorResponse( - req, - res, - new BadDataException('Invalid tenantId') - ); - } - - if ( - project.requireSsoForLogin && - !UserMiddleware.doesSsoTokenForProjectExist( - req, - tenantId, - new ObjectID(userId) - ) - ) { - return Response.sendErrorResponse( - req, - res, - new SsoAuthorizationException() - ); - } - - // get project level permissions if projectid exists in request. - const userTenantAccessPermission: UserTenantAccessPermission | null = - await AccessTokenService.getUserTenantAccessPermission( - oneuptimeRequest.userAuthorization.userId, - tenantId - ); - - if (userTenantAccessPermission) { - oneuptimeRequest.userTenantAccessPermission = {}; - oneuptimeRequest.userTenantAccessPermission[ - tenantId.toString() - ] = userTenantAccessPermission; + if (userTenantAccessPermission) { + oneuptimeRequest.userTenantAccessPermission = {}; + oneuptimeRequest.userTenantAccessPermission[ + tenantId.toString() + ] = userTenantAccessPermission; + } + } catch (error) { + return Response.sendErrorResponse(req, res, error as Exception); } } if (req.headers['is-multi-tenant-query']) { - oneuptimeRequest.userTenantAccessPermission = {}; - if ( userGlobalAccessPermission && userGlobalAccessPermission.projectIds && userGlobalAccessPermission.projectIds.length > 0 ) { - const projects: Array = await ProjectService.findBy({ - query: { - _id: QueryHelper.in( - userGlobalAccessPermission?.projectIds.map( - (i: ObjectID) => { - return i.toString(); - } - ) || [] - ), - }, - select: { - requireSsoForLogin: true, - }, - limit: LIMIT_PER_PROJECT, - skip: 0, - props: { - isRoot: true, - }, - }); - - for (const projectId of userGlobalAccessPermission?.projectIds || - []) { - // check if the force sso login is required. and if it is, then check then token. - - if ( - projects.find((p: Project) => { - return ( - p._id === projectId.toString() && - p.requireSsoForLogin - ); - }) && - !UserMiddleware.doesSsoTokenForProjectExist( - req, - projectId, - new ObjectID(userId) - ) - ) { - // Add default permissions. - const userTenantAccessPermission: UserTenantAccessPermission | null = - AccessTokenService.getDefaultUserTenantAccessPermission( - projectId - ); - oneuptimeRequest.userTenantAccessPermission[ - projectId.toString() - ] = userTenantAccessPermission; - } else { - // get project level permissions if projectid exists in request. - const userTenantAccessPermission: UserTenantAccessPermission | null = - await AccessTokenService.getUserTenantAccessPermission( - oneuptimeRequest.userAuthorization.userId, - projectId - ); - - if (userTenantAccessPermission) { - oneuptimeRequest.userTenantAccessPermission[ - projectId.toString() - ] = userTenantAccessPermission; - } - } + const userTenantAccessPermission: Dictionary | null = + await this.getUserTenantAccessPermissionForMultiTenant( + req, + userObjectId, + userGlobalAccessPermission.projectIds + ); + if (userTenantAccessPermission) { + oneuptimeRequest.userTenantAccessPermission = + userTenantAccessPermission; } } } @@ -343,4 +264,106 @@ export default class UserMiddleware { return next(); } + + public static async getUserTenantAccessPermissionWithTenantId( + req: ExpressRequest, + tenantId: ObjectID, + userId: ObjectID + ): Promise { + const project: Project | null = await ProjectService.findOneById({ + id: tenantId, + select: { + requireSsoForLogin: true, + }, + props: { + isRoot: true, + }, + }); + + if (!project) { + throw new BadDataException('Invalid tenantId'); + } + + if ( + project.requireSsoForLogin && + !UserMiddleware.doesSsoTokenForProjectExist(req, tenantId, userId) + ) { + throw new SsoAuthorizationException(); + } + + // get project level permissions if projectid exists in request. + return await AccessTokenService.getUserTenantAccessPermission( + userId, + tenantId + ); + } + + public static async getUserTenantAccessPermissionForMultiTenant( + req: ExpressRequest, + userId: ObjectID, + projectIds: ObjectID[] + ): Promise | null> { + if (!projectIds.length) { + return null; + } + + const projects: Array = await ProjectService.findBy({ + query: { + _id: QueryHelper.in( + projectIds.map((i: ObjectID) => { + return i.toString(); + }) || [] + ), + }, + select: { + requireSsoForLogin: true, + }, + limit: LIMIT_PER_PROJECT, + skip: 0, + props: { + isRoot: true, + }, + }); + + let result: Dictionary | null = null; + for (const projectId of projectIds) { + // check if the force sso login is required. and if it is, then check then token. + + let userTenantAccessPermission: UserTenantAccessPermission | null; + if ( + projects.find((p: Project) => { + return ( + p._id === projectId.toString() && p.requireSsoForLogin + ); + }) && + !UserMiddleware.doesSsoTokenForProjectExist( + req, + projectId, + userId + ) + ) { + // Add default permissions. + userTenantAccessPermission = + AccessTokenService.getDefaultUserTenantAccessPermission( + projectId + ); + } else { + // get project level permissions if projectid exists in request. + userTenantAccessPermission = + await AccessTokenService.getUserTenantAccessPermission( + userId, + projectId + ); + } + + if (userTenantAccessPermission) { + if (!result) { + result = {}; + } + result[projectId.toString()] = userTenantAccessPermission; + } + } + + return result; + } } From bf31b15930221c993825c0b1e7ca556fada88913 Mon Sep 17 00:00:00 2001 From: Abolaji Oyerinde Date: Sun, 4 Jun 2023 21:00:19 +0100 Subject: [PATCH 4/5] test: refactored and added more unit tests --- .../Middleware/UserAuthorization.test.ts | 545 +++++++++++++----- 1 file changed, 391 insertions(+), 154 deletions(-) diff --git a/CommonServer/Tests/Middleware/UserAuthorization.test.ts b/CommonServer/Tests/Middleware/UserAuthorization.test.ts index 47a83e7763..a8e0f37801 100644 --- a/CommonServer/Tests/Middleware/UserAuthorization.test.ts +++ b/CommonServer/Tests/Middleware/UserAuthorization.test.ts @@ -33,12 +33,15 @@ jest.mock('../../Services/AccessTokenService'); jest.mock('../../Utils/Response'); jest.mock('../../Services/ProjectService'); jest.mock('Common/Types/HashedString'); +jest.mock('Common/Types/JSONFunctions'); type StringOrNull = string | null; describe('UserMiddleware', () => { const mockedAccessToken: string = ObjectID.generate().toString(); const projectId: ObjectID = ObjectID.generate(); + const userId: ObjectID = ObjectID.generate(); + const mockedProject: Project = { _id: projectId.toString() } as Project; beforeEach(() => { jest.clearAllMocks(); @@ -165,7 +168,6 @@ describe('UserMiddleware', () => { describe('doesSsoTokenForProjectExist', () => { const req: ExpressRequest = {} as ExpressRequest; - const userId: ObjectID = ObjectID.generate(); beforeAll(() => { jest.spyOn(UserMiddleware, 'getSsoTokens').mockReturnValue({ @@ -262,14 +264,18 @@ describe('UserMiddleware', () => { describe('getUserMiddleware', () => { let req: ExpressRequest; let res: ExpressResponse; - const next: NextFunction = jest.fn(); - const mockedUserId: ObjectID = ObjectID.generate(); const hashValue: string = 'hash-value'; + const mockedTenantAccessPermission: UserTenantAccessPermission = { + projectId, + } as UserTenantAccessPermission; + const mockedGlobalAccessPermission: UserGlobalAccessPermission = { + projectIds: [projectId], + } as UserGlobalAccessPermission; const jwtTokenData: JSONWebTokenData = { - userId: mockedUserId, + userId, isMasterAdmin: true, email: new Email('test@gmail.com'), }; @@ -293,6 +299,10 @@ describe('UserMiddleware', () => { res.set = jest.fn(); }); + afterEach(() => { + jest.clearAllMocks(); + }); + test('should call isValidProjectIdAndApiKeyMiddleware and return when hasApiKey returns true', async () => { jest.spyOn(ProjectMiddleware, 'hasApiKey').mockReturnValueOnce( true @@ -311,7 +321,7 @@ describe('UserMiddleware', () => { expect(spyGetAccessToken).not.toHaveBeenCalled(); }); - test("should call function 'next' and return, when getAccessToken returns a string value", async () => { + test("should call function 'next' and return, when getAccessToken returns a null value", async () => { const spyGetAccessToken: jest.SpyInstance = jest .spyOn(UserMiddleware, 'getAccessToken') .mockReturnValueOnce(null); @@ -333,33 +343,23 @@ describe('UserMiddleware', () => { }); await UserMiddleware.getUserMiddleware(req, res, next); + expect(next).toHaveBeenCalled(); expect(spyJWTDecode).toHaveBeenCalledWith(mockedAccessToken); expect(UserService.updateOneBy).not.toHaveBeenCalled(); }); test('should set global-permissions and global-permissions-hash in the response header, when user has global access permission', async () => { - const mockedGlobalAccessPermission: UserGlobalAccessPermission = - {} as UserGlobalAccessPermission; - jest.spyOn(ProjectMiddleware, 'getProjectId').mockReturnValueOnce( null ); - const spySerialize: jest.SpyInstance = jest - .spyOn(JSONFunctions, 'serialize') - .mockReturnValueOnce({}); + jest.spyOn(JSONFunctions, 'serialize').mockReturnValueOnce({}); const spyGetUserGlobalAccessPermission: jest.SpyInstance = jest .spyOn(AccessTokenService, 'getUserGlobalAccessPermission') .mockResolvedValueOnce(mockedGlobalAccessPermission); await UserMiddleware.getUserMiddleware(req, res, next); - expect(spyGetUserGlobalAccessPermission).toHaveBeenCalledWith( - jwtTokenData.userId - ); - expect(spySerialize).toHaveBeenCalledWith( - mockedGlobalAccessPermission - ); expect(res.set).toHaveBeenCalledWith( 'global-permissions', JSON.stringify({}) @@ -369,6 +369,9 @@ describe('UserMiddleware', () => { hashValue ); expect(next).toHaveBeenCalled(); + expect(spyGetUserGlobalAccessPermission).toHaveBeenCalledWith( + userId + ); }); test('should not set global-permissions and global-permissions-hash in the response header, when user does not have global access permission', async () => { @@ -381,9 +384,6 @@ describe('UserMiddleware', () => { await UserMiddleware.getUserMiddleware(req, res, next); - expect(spyGetUserGlobalAccessPermission).toHaveBeenCalledWith( - jwtTokenData.userId - ); expect(res.set).not.toHaveBeenCalledWith( 'global-permissions', expect.anything() @@ -393,20 +393,211 @@ describe('UserMiddleware', () => { expect.anything() ); expect(next).toHaveBeenCalled(); + expect(spyGetUserGlobalAccessPermission).toHaveBeenCalledWith( + userId + ); }); - test('should return Invalid tenantId error, when tenantId is not null and project is not found', async () => { - const spyFindOneById: jest.SpyInstance = jest - .spyOn(ProjectService, 'findOneById') - .mockResolvedValueOnce(null); + test('should call Response.sendErrorResponse, when tenantId is passed in the header and getUserTenantAccessPermissionWithTenantId throws an exception', async () => { + const spyGetUserTenantAccessPermissionWithTenantId: jest.SpyInstance = + jest + .spyOn( + UserMiddleware, + 'getUserTenantAccessPermissionWithTenantId' + ) + .mockRejectedValueOnce(new SsoAuthorizationException()); await UserMiddleware.getUserMiddleware(req, res, next); expect(Response.sendErrorResponse).toHaveBeenCalledWith( req, res, - new BadDataException('Invalid tenantId') + new SsoAuthorizationException() ); + expect( + spyGetUserTenantAccessPermissionWithTenantId + ).toHaveBeenCalledWith(req, projectId, userId); + expect(next).not.toBeCalled(); + }); + + test('should set project-permissions and project-permissions-hash in the response header, when tenantId is passed in the header and user has tenant access permission', async () => { + jest.spyOn(JSONFunctions, 'serialize').mockReturnValueOnce({}); + const spyGetUserTenantAccessPermissionWithTenantId: jest.SpyInstance = + jest + .spyOn( + UserMiddleware, + 'getUserTenantAccessPermissionWithTenantId' + ) + .mockResolvedValueOnce(mockedTenantAccessPermission); + + await UserMiddleware.getUserMiddleware(req, res, next); + + expect(res.set).toHaveBeenCalledWith( + 'project-permissions', + JSON.stringify({}) + ); + expect(res.set).toHaveBeenCalledWith( + 'project-permissions-hash', + hashValue + ); + expect(next).toHaveBeenCalled(); + + expect( + spyGetUserTenantAccessPermissionWithTenantId + ).toHaveBeenCalledWith(req, projectId, userId); + }); + + test("should not call getUserTenantAccessPermissionForMultiTenant, when is-multi-tenant-query is set in the request header and but userGlobalAccessPermission's projectIds length is zero", async () => { + const mockedRequest: Partial = { + headers: { 'is-multi-tenant-query': 'yes' }, + }; + + jest.spyOn( + AccessTokenService, + 'getUserGlobalAccessPermission' + ).mockResolvedValueOnce({ + ...mockedGlobalAccessPermission, + projectIds: [], + }); + jest.spyOn( + UserMiddleware, + 'getUserTenantAccessPermissionWithTenantId' + ).mockResolvedValueOnce(null); + const spyGetUserTenantAccessPermissionForMultiTenant: jest.SpyInstance = + jest.spyOn( + UserMiddleware, + 'getUserTenantAccessPermissionForMultiTenant' + ); + + await UserMiddleware.getUserMiddleware( + mockedRequest as ExpressRequest, + res, + next + ); + + expect(res.set).not.toHaveBeenCalledWith( + 'project-permissions', + expect.anything() + ); + expect(res.set).not.toHaveBeenCalledWith( + 'project-permissions-hash', + expect.anything() + ); + expect(next).toHaveBeenCalled(); + expect( + spyGetUserTenantAccessPermissionForMultiTenant + ).not.toHaveBeenCalled(); + }); + + test('should set project-permissions and project-permissions-hash in the response header, when is-multi-tenant-query is set in the request header and getUserTenantAccessPermissionForMultiTenant returned access permission', async () => { + const mockedRequest: Partial = { + headers: { 'is-multi-tenant-query': 'yes' }, + }; + + jest.spyOn(JSONFunctions, 'serialize').mockReturnValue({}); + jest.spyOn( + AccessTokenService, + 'getUserGlobalAccessPermission' + ).mockResolvedValueOnce(mockedGlobalAccessPermission); + jest.spyOn( + UserMiddleware, + 'getUserTenantAccessPermissionWithTenantId' + ).mockResolvedValueOnce(null); + const spyGetUserTenantAccessPermissionForMultiTenant: jest.SpyInstance = + jest + .spyOn( + UserMiddleware, + 'getUserTenantAccessPermissionForMultiTenant' + ) + .mockResolvedValueOnce({ + [projectId.toString()]: mockedTenantAccessPermission, + }); + + await UserMiddleware.getUserMiddleware( + mockedRequest as ExpressRequest, + res, + next + ); + + expect(res.set).toHaveBeenCalledWith( + 'project-permissions', + JSON.stringify({}) + ); + expect(res.set).toHaveBeenCalledWith( + 'project-permissions-hash', + hashValue + ); + expect(next).toHaveBeenCalled(); + expect( + spyGetUserTenantAccessPermissionForMultiTenant + ).toHaveBeenCalledWith( + mockedRequest, + userId, + mockedGlobalAccessPermission.projectIds + ); + }); + + test('should not set project-permissions and project-permissions-hash in the response header, when the project-permissions-hash set in the request header equals the projectPermissionsHash computed from userTenantAccessPermission', async () => { + const mockedRequest: Partial = { + headers: { 'project-permissions-hash': hashValue }, + }; + + const spyGetUserTenantAccessPermissionWithTenantId: jest.SpyInstance = + jest + .spyOn( + UserMiddleware, + 'getUserTenantAccessPermissionWithTenantId' + ) + .mockResolvedValueOnce(mockedTenantAccessPermission); + + await UserMiddleware.getUserMiddleware( + mockedRequest as ExpressRequest, + res, + next + ); + + expect(res.set).not.toHaveBeenCalledWith( + 'project-permissions', + expect.anything() + ); + expect(res.set).not.toHaveBeenCalledWith( + 'project-permissions-hash', + expect.anything() + ); + expect(next).toHaveBeenCalled(); + + expect( + spyGetUserTenantAccessPermissionWithTenantId + ).toHaveBeenCalledWith(mockedRequest, projectId, userId); + }); + }); + + describe('getUserTenantAccessPermissionWithTenantId', () => { + const req: ExpressRequest = {} as ExpressRequest; + + const spyFindOneById: jest.SpyInstance = jest.spyOn( + ProjectService, + 'findOneById' + ); + const spyDoesSsoTokenForProjectExist: jest.SpyInstance = jest.spyOn( + UserMiddleware, + 'doesSsoTokenForProjectExist' + ); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test("should throw 'Invalid tenantId' error, when project is not found for the tenantId", async () => { + spyFindOneById.mockResolvedValueOnce(null); + + await expect( + UserMiddleware.getUserTenantAccessPermissionWithTenantId( + req, + projectId, + userId + ) + ).rejects.toThrowError(new BadDataException('Invalid tenantId')); expect(spyFindOneById).toHaveBeenCalledWith({ id: projectId, select: { @@ -418,98 +609,110 @@ describe('UserMiddleware', () => { }); }); - test('should return SSO Authorization Required error, when sso is required for login but sso token for project does not exist', async () => { - jest.spyOn(ProjectService, 'findOneById').mockResolvedValueOnce({ + test('should throw SsoAuthorizationException error, when sso login is required but sso token for the projectId does not exist', async () => { + spyFindOneById.mockResolvedValueOnce({ + ...mockedProject, requireSsoForLogin: true, - } as Project); + }); - const spyDoesSsoTokenForProjectExist: jest.SpyInstance = jest - .spyOn(UserMiddleware, 'doesSsoTokenForProjectExist') - .mockReturnValueOnce(false); + spyDoesSsoTokenForProjectExist.mockReturnValueOnce(false); - await UserMiddleware.getUserMiddleware(req, res, next); - - expect(Response.sendErrorResponse).toHaveBeenCalledWith( - req, - res, - new SsoAuthorizationException() - ); + await expect( + UserMiddleware.getUserTenantAccessPermissionWithTenantId( + req, + projectId, + userId + ) + ).rejects.toThrowError(new SsoAuthorizationException()); expect(spyDoesSsoTokenForProjectExist).toHaveBeenCalledWith( req, projectId, - jwtTokenData.userId + userId ); }); - test('should set project-permissions and project-permissions-hash in the response header when tenantId is not null and user has tenant access permission', async () => { - const mockedTenantAccessPermission: UserTenantAccessPermission = - {} as UserTenantAccessPermission; + test('should return null when getUserTenantAccessPermission returns null', async () => { + spyFindOneById.mockResolvedValueOnce(mockedProject); - jest.spyOn(ProjectService, 'findOneById').mockResolvedValueOnce({ - requireSsoForLogin: false, - } as Project); - const spySerialize: jest.SpyInstance = jest - .spyOn(JSONFunctions, 'serialize') - .mockReturnValueOnce({}); const spyGetUserTenantAccessPermission: jest.SpyInstance = jest .spyOn(AccessTokenService, 'getUserTenantAccessPermission') - .mockResolvedValueOnce(mockedTenantAccessPermission); + .mockResolvedValueOnce(null); - await UserMiddleware.getUserMiddleware(req, res, next); + const result: UserTenantAccessPermission | null = + await UserMiddleware.getUserTenantAccessPermissionWithTenantId( + req, + projectId, + userId + ); - expect(spyGetUserTenantAccessPermission).toHaveBeenCalledWith( - jwtTokenData.userId, + expect(result).toBeNull(); + expect(spyGetUserTenantAccessPermission).toHaveBeenLastCalledWith( + userId, projectId ); - expect(spySerialize).toHaveBeenCalledWith( - mockedTenantAccessPermission - ); - expect(res.set).toHaveBeenCalledWith( - 'project-permissions', - JSON.stringify({}) - ); - expect(res.set).toHaveBeenCalledWith( - 'project-permissions-hash', - hashValue - ); - expect(next).toHaveBeenCalled(); }); - test('should set project-permissions and project-permissions-hash in the response header with default tenant access permission, when is-multi-tenant-query request header is set and sso-token does not exist', async () => { - const mockedTenantAccessPermission: UserTenantAccessPermission = - {} as UserTenantAccessPermission; - const mockedGlobalAccessPermission: UserGlobalAccessPermission = { - projectIds: [projectId], - } as UserGlobalAccessPermission; - const project: Project = { - _id: projectId.toString(), - requireSsoForLogin: true, - } as Project; + test('should return UserTenantAccessPermission', async () => { + const mockedUserTenantAccessPermission: UserTenantAccessPermission = + { projectId } as UserTenantAccessPermission; - const mockedRequest: Partial = { - headers: { 'is-multi-tenant-query': 'yes' }, - }; + spyFindOneById.mockResolvedValueOnce(mockedProject); - jest.spyOn(ProjectService, 'findBy').mockResolvedValueOnce([ - project, + const spyGetUserTenantAccessPermission: jest.SpyInstance = jest + .spyOn(AccessTokenService, 'getUserTenantAccessPermission') + .mockResolvedValueOnce(mockedUserTenantAccessPermission); + + const result: UserTenantAccessPermission | null = + await UserMiddleware.getUserTenantAccessPermissionWithTenantId( + req, + projectId, + userId + ); + + expect(result).toEqual(mockedUserTenantAccessPermission); + expect(spyGetUserTenantAccessPermission).toHaveBeenLastCalledWith( + userId, + projectId + ); + }); + }); + + describe('getUserTenantAccessPermissionForMultiTenant', () => { + const req: ExpressRequest = {} as ExpressRequest; + const mockedUserTenantAccessPermission: UserTenantAccessPermission = { + projectId, + } as UserTenantAccessPermission; + + const spyFindBy: jest.SpyInstance = jest.spyOn( + ProjectService, + 'findBy' + ); + const spyDoesSsoTokenForProjectExist: jest.SpyInstance = jest.spyOn( + UserMiddleware, + 'doesSsoTokenForProjectExist' + ); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('should return null, when projectIds length is zero', async () => { + const result: Dictionary | null = + await UserMiddleware.getUserTenantAccessPermissionForMultiTenant( + req, + userId, + [] + ); + + expect(result).toBeNull(); + expect(spyFindBy).not.toBeCalled(); + }); + + test('should return default tenant access permission, when project for a projectId is found, sso is required for login, but sso token does not exist for that projectId', async () => { + spyDoesSsoTokenForProjectExist.mockReturnValueOnce(false); + spyFindBy.mockResolvedValueOnce([ + { ...mockedProject, requireSsoForLogin: true }, ]); - jest.spyOn(ProjectService, 'findOneById').mockResolvedValueOnce({ - ...project, - requireSsoForLogin: false, - } as Project); - jest.spyOn( - UserMiddleware, - 'doesSsoTokenForProjectExist' - ).mockReturnValueOnce(false); - jest.spyOn(JSONFunctions, 'serialize').mockReturnValueOnce({}); - jest.spyOn( - AccessTokenService, - 'getUserGlobalAccessPermission' - ).mockResolvedValueOnce(mockedGlobalAccessPermission); - jest.spyOn( - AccessTokenService, - 'getUserTenantAccessPermission' - ).mockResolvedValueOnce(null); const spyGetDefaultUserTenantAccessPermission: jest.SpyInstance = jest @@ -517,78 +720,112 @@ describe('UserMiddleware', () => { AccessTokenService, 'getDefaultUserTenantAccessPermission' ) - .mockReturnValueOnce(mockedTenantAccessPermission); + .mockReturnValueOnce(mockedUserTenantAccessPermission); - await UserMiddleware.getUserMiddleware( - mockedRequest as ExpressRequest, - res, - next + const result: Dictionary | null = + await UserMiddleware.getUserTenantAccessPermissionForMultiTenant( + req, + userId, + [projectId] + ); + + expect(result).toEqual({ + [projectId.toString()]: mockedUserTenantAccessPermission, + }); + expect(spyDoesSsoTokenForProjectExist).toHaveBeenCalledWith( + req, + projectId, + userId ); - expect( spyGetDefaultUserTenantAccessPermission ).toHaveBeenCalledWith(projectId); - expect(res.set).toHaveBeenCalledWith( - 'project-permissions', - JSON.stringify({}) - ); - expect(res.set).toHaveBeenCalledWith( - 'project-permissions-hash', - hashValue - ); - expect(next).toHaveBeenCalled(); }); - test('should set project-permissions and project-permissions-hash in the response header with user tenant access permission, when is-multi-tenant-query request header is set and user has tenant access permission', async () => { - const mockedTenantAccessPermission: UserTenantAccessPermission = - {} as UserTenantAccessPermission; - const mockedGlobalAccessPermission: UserGlobalAccessPermission = { - projectIds: [projectId], - } as UserGlobalAccessPermission; - const project: Project = { - _id: projectId.toString(), - requireSsoForLogin: true, - } as Project; + test('should return user tenant access permission, when project for a projectId is found, sso is not required for login and project level permission exist for the projectId', async () => { + spyFindBy.mockResolvedValueOnce([mockedProject]); - const mockedRequest: Partial = { - headers: { 'is-multi-tenant-query': 'yes' }, - }; + const spyGetUserTenantAccessPermission: jest.SpyInstance = jest + .spyOn(AccessTokenService, 'getUserTenantAccessPermission') + .mockResolvedValueOnce(mockedUserTenantAccessPermission); + + const result: Dictionary | null = + await UserMiddleware.getUserTenantAccessPermissionForMultiTenant( + req, + userId, + [projectId] + ); + + expect(result).toEqual({ + [projectId.toString()]: mockedUserTenantAccessPermission, + }); + expect(spyDoesSsoTokenForProjectExist).not.toBeCalled(); + expect(spyGetUserTenantAccessPermission).toHaveBeenCalledWith( + userId, + projectId + ); + }); + + test('should return null, when project for a projectId is found, sso is not required for login but project level permission does not exist for the projectId', async () => { + spyFindBy.mockResolvedValueOnce([mockedProject]); + + const spyGetUserTenantAccessPermission: jest.SpyInstance = jest + .spyOn(AccessTokenService, 'getUserTenantAccessPermission') + .mockResolvedValueOnce(null); + + const result: Dictionary | null = + await UserMiddleware.getUserTenantAccessPermissionForMultiTenant( + req, + userId, + [projectId] + ); + + expect(result).toBeNull(); + expect(spyGetUserTenantAccessPermission).toHaveBeenCalledWith( + userId, + projectId + ); + }); + + test('should return user tenant access permission, when project for a projectId is not found, but project level permission exist for the projectId', async () => { + spyFindBy.mockResolvedValueOnce([]); - jest.spyOn(ProjectService, 'findBy').mockResolvedValueOnce([ - project, - ]); - jest.spyOn(ProjectService, 'findOneById').mockResolvedValueOnce({ - ...project, - requireSsoForLogin: false, - } as Project); - jest.spyOn( - UserMiddleware, - 'doesSsoTokenForProjectExist' - ).mockReturnValueOnce(true); - jest.spyOn(JSONFunctions, 'serialize').mockReturnValueOnce({}); jest.spyOn( AccessTokenService, - 'getUserGlobalAccessPermission' - ).mockResolvedValueOnce(mockedGlobalAccessPermission); - jest.spyOn(AccessTokenService, 'getUserTenantAccessPermission') - .mockResolvedValueOnce(null) - .mockResolvedValueOnce(mockedTenantAccessPermission); + 'getUserTenantAccessPermission' + ).mockResolvedValueOnce(mockedUserTenantAccessPermission); - await UserMiddleware.getUserMiddleware( - mockedRequest as ExpressRequest, - res, - next - ); + const result: Dictionary | null = + await UserMiddleware.getUserTenantAccessPermissionForMultiTenant( + req, + userId, + [projectId] + ); - expect(res.set).toHaveBeenCalledWith( - 'project-permissions', - JSON.stringify({}) + expect(result).toEqual({ + [projectId.toString()]: mockedUserTenantAccessPermission, + }); + }); + + test('should return null, when project for a projectId is not found, and project level permission does not exist for the projectId', async () => { + spyFindBy.mockResolvedValueOnce([]); + + const spyGetUserTenantAccessPermission: jest.SpyInstance = jest + .spyOn(AccessTokenService, 'getUserTenantAccessPermission') + .mockResolvedValueOnce(null); + + const result: Dictionary | null = + await UserMiddleware.getUserTenantAccessPermissionForMultiTenant( + req, + userId, + [projectId] + ); + + expect(result).toBeNull(); + expect(spyGetUserTenantAccessPermission).toHaveBeenCalledWith( + userId, + projectId ); - expect(res.set).toHaveBeenCalledWith( - 'project-permissions-hash', - hashValue - ); - expect(next).toHaveBeenCalled(); }); }); }); From c4ef932e1361c70397e9bbfdd75b176d0713aa9c Mon Sep 17 00:00:00 2001 From: Abolaji Oyerinde Date: Tue, 6 Jun 2023 14:52:52 +0100 Subject: [PATCH 5/5] fix: changed 'this' referecence to static functions to Class name --- CommonServer/Middleware/UserAuthorization.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CommonServer/Middleware/UserAuthorization.ts b/CommonServer/Middleware/UserAuthorization.ts index 8d74b79545..b8af7a42de 100644 --- a/CommonServer/Middleware/UserAuthorization.ts +++ b/CommonServer/Middleware/UserAuthorization.ts @@ -179,7 +179,7 @@ export default class UserMiddleware { if (tenantId) { try { const userTenantAccessPermission: UserTenantAccessPermission | null = - await this.getUserTenantAccessPermissionWithTenantId( + await UserMiddleware.getUserTenantAccessPermissionWithTenantId( req, tenantId, userObjectId @@ -203,7 +203,7 @@ export default class UserMiddleware { userGlobalAccessPermission.projectIds.length > 0 ) { const userTenantAccessPermission: Dictionary | null = - await this.getUserTenantAccessPermissionForMultiTenant( + await UserMiddleware.getUserTenantAccessPermissionForMultiTenant( req, userObjectId, userGlobalAccessPermission.projectIds