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