mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	refactor: split NoAccessError into ForbiddenError + PermissionError (#4190)
In some of the places we used `NoAccessError` for permissions, other places we used it for a more generic 403 error with a different message. This refactoring splits the error type into two distinct types instead to make the error messages more consistent.
This commit is contained in:
		
							parent
							
								
									4ce78ccecd
								
							
						
					
					
						commit
						5b95eed163
					
				
							
								
								
									
										6
									
								
								src/lib/error/forbidden-error.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								src/lib/error/forbidden-error.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,6 @@ | ||||
| import { UnleashError } from './unleash-error'; | ||||
| 
 | ||||
| class ForbiddenError extends UnleashError {} | ||||
| 
 | ||||
| export default ForbiddenError; | ||||
| module.exports = ForbiddenError; | ||||
| @ -7,13 +7,14 @@ import InvalidOperationError from './invalid-operation-error'; | ||||
| import InvalidTokenError from './invalid-token-error'; | ||||
| import MinimumOneEnvironmentError from './minimum-one-environment-error'; | ||||
| import NameExistsError from './name-exists-error'; | ||||
| import NoAccessError from './no-access-error'; | ||||
| import PermissionError from './permission-error'; | ||||
| import { OperationDeniedError } from './operation-denied-error'; | ||||
| import UserTokenError from './used-token-error'; | ||||
| import RoleInUseError from './role-in-use-error'; | ||||
| import ProjectWithoutOwnerError from './project-without-owner-error'; | ||||
| import PasswordUndefinedError from './password-undefined'; | ||||
| import PasswordMismatchError from './password-mismatch'; | ||||
| import ForbiddenError from './forbidden-error'; | ||||
| 
 | ||||
| export { | ||||
|     BadDataError, | ||||
| @ -26,7 +27,8 @@ export { | ||||
|     InvalidTokenError, | ||||
|     MinimumOneEnvironmentError, | ||||
|     NameExistsError, | ||||
|     NoAccessError, | ||||
|     PermissionError, | ||||
|     ForbiddenError, | ||||
|     OperationDeniedError, | ||||
|     UserTokenError, | ||||
|     RoleInUseError, | ||||
|  | ||||
| @ -2,7 +2,7 @@ import { ApiErrorSchema, UnleashError } from './unleash-error'; | ||||
| 
 | ||||
| type Permission = string | string[]; | ||||
| 
 | ||||
| class NoAccessError extends UnleashError { | ||||
| class PermissionError extends UnleashError { | ||||
|     permissions: Permission; | ||||
| 
 | ||||
|     constructor(permission: Permission = [], environment?: string) { | ||||
| @ -12,11 +12,13 @@ class NoAccessError extends UnleashError { | ||||
| 
 | ||||
|         const permissionsMessage = | ||||
|             permissions.length === 1 | ||||
|                 ? `the ${permissions[0]} permission` | ||||
|                 : `any of the following permissions: ${permissions.join(', ')}`; | ||||
|                 ? `the "${permissions[0]}" permission` | ||||
|                 : `all of the following permissions: ${permissions | ||||
|                       .map((perm) => `"${perm}"`) | ||||
|                       .join(', ')}`;
 | ||||
| 
 | ||||
|         const message = | ||||
|             `You don't have the required permissions to perform this operation. You need ${permissionsMessage}" to perform this action` + | ||||
|             `You don't have the required permissions to perform this operation. To perform this action, you need ${permissionsMessage}` + | ||||
|             (environment ? ` in the "${environment}" environment.` : `.`); | ||||
| 
 | ||||
|         super(message); | ||||
| @ -32,5 +34,5 @@ class NoAccessError extends UnleashError { | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export default NoAccessError; | ||||
| module.exports = NoAccessError; | ||||
| export default PermissionError; | ||||
| module.exports = PermissionError; | ||||
| @ -6,7 +6,7 @@ import BadDataError, { | ||||
|     fromOpenApiValidationError, | ||||
|     fromOpenApiValidationErrors, | ||||
| } from './bad-data-error'; | ||||
| import NoAccessError from './no-access-error'; | ||||
| import PermissionError from './permission-error'; | ||||
| import OwaspValidationError from './owasp-validation-error'; | ||||
| import IncompatibleProjectError from './incompatible-project-error'; | ||||
| import PasswordUndefinedError from './password-undefined'; | ||||
| @ -462,7 +462,7 @@ describe('Error serialization special cases', () => { | ||||
| 
 | ||||
|     it('NoAccessError: adds `permissions`', () => { | ||||
|         const permission = 'x'; | ||||
|         const error = new NoAccessError(permission); | ||||
|         const error = new PermissionError(permission); | ||||
|         const json = error.toJSON(); | ||||
| 
 | ||||
|         expect(json.permissions).toStrictEqual([permission]); | ||||
| @ -470,7 +470,7 @@ describe('Error serialization special cases', () => { | ||||
| 
 | ||||
|     it('NoAccessError: supports multiple permissions', () => { | ||||
|         const permission = ['x', 'y', 'z']; | ||||
|         const error = new NoAccessError(permission); | ||||
|         const error = new PermissionError(permission); | ||||
|         const json = error.toJSON(); | ||||
| 
 | ||||
|         expect(json.permissions).toStrictEqual(permission); | ||||
|  | ||||
| @ -24,9 +24,10 @@ export const UnleashApiErrorTypes = [ | ||||
|     'ValidationError', | ||||
|     'AuthenticationRequired', | ||||
|     'UnauthorizedError', | ||||
|     'NoAccessError', | ||||
|     'PermissionError', | ||||
|     'InvalidTokenError', | ||||
|     'OwaspValidationError', | ||||
|     'ForbiddenError', | ||||
| 
 | ||||
|     // server errors; not the end user's fault
 | ||||
|     'InternalError', | ||||
| @ -86,6 +87,10 @@ const statusCode = (errorName: string): number => { | ||||
|             return 403; | ||||
|         case 'AuthenticationRequired': | ||||
|             return 401; | ||||
|         case 'ForbiddenError': | ||||
|             return 403; | ||||
|         case 'PermissionError': | ||||
|             return 403; | ||||
|         case 'BadRequestError': //thrown by express; do not remove
 | ||||
|             return 400; | ||||
|         default: | ||||
|  | ||||
| @ -17,7 +17,7 @@ import { FeatureVariantsSchema } from '../../../openapi/spec/feature-variants-sc | ||||
| import { createRequestSchema } from '../../../openapi/util/create-request-schema'; | ||||
| import { createResponseSchema } from '../../../openapi/util/create-response-schema'; | ||||
| import { AccessService } from '../../../services'; | ||||
| import { BadDataError, NoAccessError } from '../../../../lib/error'; | ||||
| import { BadDataError, PermissionError } from '../../../../lib/error'; | ||||
| import { User } from 'lib/server-impl'; | ||||
| import { PushVariantsSchema } from 'lib/openapi/spec/push-variants-schema'; | ||||
| 
 | ||||
| @ -309,7 +309,7 @@ export default class VariantsController extends Controller { | ||||
|                     environment, | ||||
|                 )) | ||||
|             ) { | ||||
|                 throw new NoAccessError( | ||||
|                 throw new PermissionError( | ||||
|                     UPDATE_FEATURE_ENVIRONMENT_VARIANTS, | ||||
|                     environment, | ||||
|                 ); | ||||
|  | ||||
| @ -3,8 +3,8 @@ import { Logger } from 'lib/logger'; | ||||
| import { IUnleashConfig } from '../types/option'; | ||||
| import { NONE } from '../types/permissions'; | ||||
| import { handleErrors } from './util'; | ||||
| import NoAccessError from '../error/no-access-error'; | ||||
| import requireContentType from '../middleware/content_type_checker'; | ||||
| import { PermissionError } from '../error'; | ||||
| 
 | ||||
| interface IRequestHandler< | ||||
|     P = any, | ||||
| @ -52,7 +52,7 @@ const checkPermission = | ||||
|         if (req.checkRbac && (await req.checkRbac(permissions))) { | ||||
|             return next(); | ||||
|         } | ||||
|         return res.status(403).json(new NoAccessError(permissions)).end(); | ||||
|         return res.status(403).json(new PermissionError(permissions)).end(); | ||||
|     }; | ||||
| 
 | ||||
| /** | ||||
|  | ||||
| @ -44,7 +44,12 @@ import { Logger } from '../logger'; | ||||
| import BadDataError from '../error/bad-data-error'; | ||||
| import NameExistsError from '../error/name-exists-error'; | ||||
| import InvalidOperationError from '../error/invalid-operation-error'; | ||||
| import { FOREIGN_KEY_VIOLATION, OperationDeniedError } from '../error'; | ||||
| import { | ||||
|     FOREIGN_KEY_VIOLATION, | ||||
|     OperationDeniedError, | ||||
|     PermissionError, | ||||
|     ForbiddenError, | ||||
| } from '../error'; | ||||
| import { | ||||
|     constraintSchema, | ||||
|     featureMetadataSchema, | ||||
| @ -79,7 +84,6 @@ import { | ||||
| } from '../features/playground/feature-evaluator/helpers'; | ||||
| import { AccessService } from './access-service'; | ||||
| import { User } from '../server-impl'; | ||||
| import NoAccessError from '../error/no-access-error'; | ||||
| import { IFeatureProjectUserParams } from '../routes/admin-api/project/project-features'; | ||||
| import { unique } from '../util/unique'; | ||||
| import { ISegmentService } from 'lib/segments/segment-service-interface'; | ||||
| @ -976,7 +980,7 @@ class FeatureToggleService { | ||||
|                 projectId, | ||||
|             ); | ||||
|         if (changeRequestEnabled) { | ||||
|             throw new NoAccessError( | ||||
|             throw new ForbiddenError( | ||||
|                 `Cloning not allowed. Project ${projectId} has change requests enabled.`, | ||||
|             ); | ||||
|         } | ||||
| @ -1517,7 +1521,7 @@ class FeatureToggleService { | ||||
|                 newProject, | ||||
|             ); | ||||
|         if (changeRequestEnabled) { | ||||
|             throw new NoAccessError( | ||||
|             throw new ForbiddenError( | ||||
|                 `Changing project not allowed. Project ${newProject} has change requests enabled.`, | ||||
|             ); | ||||
|         } | ||||
| @ -1918,7 +1922,7 @@ class FeatureToggleService { | ||||
|                 user, | ||||
|             ); | ||||
|         if (!canBypass) { | ||||
|             throw new NoAccessError(SKIP_CHANGE_REQUEST); | ||||
|             throw new PermissionError(SKIP_CHANGE_REQUEST); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| @ -1950,7 +1954,7 @@ class FeatureToggleService { | ||||
|                         environment, | ||||
|                     )); | ||||
|                 if (!canAddStrategies) { | ||||
|                     throw new NoAccessError(CREATE_FEATURE_STRATEGY); | ||||
|                     throw new PermissionError(CREATE_FEATURE_STRATEGY); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
| @ -47,7 +47,6 @@ import { | ||||
|     IRoleDescriptor, | ||||
| } from '../types/stores/access-store'; | ||||
| import FeatureToggleService from './feature-toggle-service'; | ||||
| import NoAccessError from '../error/no-access-error'; | ||||
| import IncompatibleProjectError from '../error/incompatible-project-error'; | ||||
| import { IFeatureTagStore } from 'lib/types/stores/feature-tag-store'; | ||||
| import ProjectWithoutOwnerError from '../error/project-without-owner-error'; | ||||
| @ -58,6 +57,7 @@ import { FavoritesService } from './favorites-service'; | ||||
| import { calculateAverageTimeToProd } from '../features/feature-toggle/time-to-production/time-to-production'; | ||||
| import { IProjectStatsStore } from 'lib/types/stores/project-stats-store-type'; | ||||
| import { uniqueByKey } from '../util/unique'; | ||||
| import { PermissionError } from '../error'; | ||||
| 
 | ||||
| const getCreatedBy = (user: IUser) => user.email || user.username || 'unknown'; | ||||
| 
 | ||||
| @ -259,7 +259,7 @@ export default class ProjectService { | ||||
|         const feature = await this.featureToggleStore.get(featureName); | ||||
| 
 | ||||
|         if (feature.project !== currentProjectId) { | ||||
|             throw new NoAccessError(MOVE_FEATURE_TOGGLE); | ||||
|             throw new PermissionError(MOVE_FEATURE_TOGGLE); | ||||
|         } | ||||
|         const project = await this.getProject(newProjectId); | ||||
| 
 | ||||
| @ -274,7 +274,7 @@ export default class ProjectService { | ||||
|         ); | ||||
| 
 | ||||
|         if (!authorized) { | ||||
|             throw new NoAccessError(MOVE_FEATURE_TOGGLE); | ||||
|             throw new PermissionError(MOVE_FEATURE_TOGGLE); | ||||
|         } | ||||
| 
 | ||||
|         const isCompatibleWithTargetProject = | ||||
|  | ||||
| @ -776,7 +776,7 @@ test('Should be denied move feature toggle to project where the user does not ha | ||||
|             projectOrigin.id, | ||||
|         ); | ||||
|     } catch (e) { | ||||
|         expect(e.name).toContain('NoAccess'); | ||||
|         expect(e.name).toContain('Permission'); | ||||
|         expect(e.message.includes('permission')).toBeTruthy(); | ||||
|         expect( | ||||
|             e.message.includes(permissions.MOVE_FEATURE_TOGGLE), | ||||
|  | ||||
| @ -11,7 +11,7 @@ import { FeatureStrategySchema } from '../../../lib/openapi'; | ||||
| import User from '../../../lib/types/user'; | ||||
| import { IConstraint, IVariant, SKIP_CHANGE_REQUEST } from '../../../lib/types'; | ||||
| import EnvironmentService from '../../../lib/services/environment-service'; | ||||
| import { NoAccessError } from '../../../lib/error'; | ||||
| import { ForbiddenError, PermissionError } from '../../../lib/error'; | ||||
| import { ISegmentService } from '../../../lib/segments/segment-service-interface'; | ||||
| import { ChangeRequestAccessReadModel } from '../../../lib/features/change-request-access-service/sql-change-request-access-read-model'; | ||||
| 
 | ||||
| @ -350,7 +350,7 @@ test('cloning a feature toggle not allowed for change requests enabled', async ( | ||||
|             'test-user', | ||||
|         ), | ||||
|     ).rejects.toEqual( | ||||
|         new NoAccessError( | ||||
|         new ForbiddenError( | ||||
|             `Cloning not allowed. Project default has change requests enabled.`, | ||||
|         ), | ||||
|     ); | ||||
| @ -364,7 +364,7 @@ test('changing to a project with change requests enabled should not be allowed', | ||||
|     await expect( | ||||
|         service.changeProject('newToggleName', 'default', 'user'), | ||||
|     ).rejects.toEqual( | ||||
|         new NoAccessError( | ||||
|         new ForbiddenError( | ||||
|             `Changing project not allowed. Project default has change requests enabled.`, | ||||
|         ), | ||||
|     ); | ||||
| @ -484,7 +484,7 @@ test('If change requests are enabled, cannot change variants without going via C | ||||
|             }, | ||||
|             [], | ||||
|         ), | ||||
|     ).rejects.toThrowError(new NoAccessError(SKIP_CHANGE_REQUEST)); | ||||
|     ).rejects.toThrowError(new PermissionError(SKIP_CHANGE_REQUEST)); | ||||
| }); | ||||
| 
 | ||||
| test('If CRs are protected for any environment in the project stops bulk update of variants', async () => { | ||||
| @ -590,7 +590,7 @@ test('If CRs are protected for any environment in the project stops bulk update | ||||
|                 isAPI: true, | ||||
|             }, | ||||
|         ), | ||||
|     ).rejects.toThrowError(new NoAccessError(SKIP_CHANGE_REQUEST)); | ||||
|     ).rejects.toThrowError(new PermissionError(SKIP_CHANGE_REQUEST)); | ||||
| }); | ||||
| 
 | ||||
| test('getPlaygroundFeatures should return ids and titles (if they exist) on client strategies', async () => { | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user