mirror of
https://github.com/Unleash/unleash.git
synced 2025-06-04 01:18:20 +02:00
fix: always require permission for POST, PATCH, PUT, DELETE (#1152)
This commit is contained in:
parent
dd982d5a08
commit
3c550f157a
@ -41,7 +41,7 @@ class ContextController extends Controller {
|
|||||||
this.deleteContextField,
|
this.deleteContextField,
|
||||||
DELETE_CONTEXT_FIELD,
|
DELETE_CONTEXT_FIELD,
|
||||||
);
|
);
|
||||||
this.post('/validate', this.validate);
|
this.post('/validate', this.validate, UPDATE_CONTEXT_FIELD);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getContextFields(req: Request, res: Response): Promise<void> {
|
async getContextFields(req: Request, res: Response): Promise<void> {
|
||||||
|
@ -44,7 +44,7 @@ class FeatureController extends Controller {
|
|||||||
this.get('/:featureName', this.getToggle);
|
this.get('/:featureName', this.getToggle);
|
||||||
this.put('/:featureName', this.updateToggle, UPDATE_FEATURE);
|
this.put('/:featureName', this.updateToggle, UPDATE_FEATURE);
|
||||||
this.delete('/:featureName', this.archiveToggle, DELETE_FEATURE);
|
this.delete('/:featureName', this.archiveToggle, DELETE_FEATURE);
|
||||||
this.post('/validate', this.validate);
|
this.post('/validate', this.validate, UPDATE_FEATURE);
|
||||||
this.post('/:featureName/toggle', this.toggle, UPDATE_FEATURE);
|
this.post('/:featureName/toggle', this.toggle, UPDATE_FEATURE);
|
||||||
this.post('/:featureName/toggle/on', this.toggleOn, UPDATE_FEATURE);
|
this.post('/:featureName/toggle/on', this.toggleOn, UPDATE_FEATURE);
|
||||||
this.post('/:featureName/toggle/off', this.toggleOff, UPDATE_FEATURE);
|
this.post('/:featureName/toggle/off', this.toggleOff, UPDATE_FEATURE);
|
||||||
|
@ -5,7 +5,11 @@ import { IUnleashConfig } from '../../../types/option';
|
|||||||
import { IUnleashServices } from '../../../types/services';
|
import { IUnleashServices } from '../../../types/services';
|
||||||
import FeatureToggleService from '../../../services/feature-toggle-service';
|
import FeatureToggleService from '../../../services/feature-toggle-service';
|
||||||
import { Logger } from '../../../logger';
|
import { Logger } from '../../../logger';
|
||||||
import { CREATE_FEATURE, UPDATE_FEATURE } from '../../../types/permissions';
|
import {
|
||||||
|
CREATE_FEATURE,
|
||||||
|
DELETE_FEATURE,
|
||||||
|
UPDATE_FEATURE,
|
||||||
|
} from '../../../types/permissions';
|
||||||
import {
|
import {
|
||||||
FeatureToggleDTO,
|
FeatureToggleDTO,
|
||||||
IConstraint,
|
IConstraint,
|
||||||
@ -84,9 +88,9 @@ export default class ProjectFeaturesController extends Controller {
|
|||||||
this.post(PATH_FEATURE_CLONE, this.cloneFeature, CREATE_FEATURE);
|
this.post(PATH_FEATURE_CLONE, this.cloneFeature, CREATE_FEATURE);
|
||||||
|
|
||||||
this.get(PATH_FEATURE, this.getFeature);
|
this.get(PATH_FEATURE, this.getFeature);
|
||||||
this.put(PATH_FEATURE, this.updateFeature);
|
this.put(PATH_FEATURE, this.updateFeature, UPDATE_FEATURE);
|
||||||
this.patch(PATH_FEATURE, this.patchFeature);
|
this.patch(PATH_FEATURE, this.patchFeature, UPDATE_FEATURE);
|
||||||
this.delete(PATH_FEATURE, this.archiveFeature);
|
this.delete(PATH_FEATURE, this.archiveFeature, DELETE_FEATURE);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getFeatures(
|
async getFeatures(
|
||||||
|
@ -25,7 +25,7 @@ class TagTypeController extends Controller {
|
|||||||
this.tagTypeService = tagTypeService;
|
this.tagTypeService = tagTypeService;
|
||||||
this.get('/', this.getTagTypes);
|
this.get('/', this.getTagTypes);
|
||||||
this.post('/', this.createTagType, UPDATE_TAG_TYPE);
|
this.post('/', this.createTagType, UPDATE_TAG_TYPE);
|
||||||
this.post('/validate', this.validate);
|
this.post('/validate', this.validate, UPDATE_TAG_TYPE);
|
||||||
this.get('/:name', this.getTagType);
|
this.get('/:name', this.getTagType);
|
||||||
this.put('/:name', this.updateTagType, UPDATE_TAG_TYPE);
|
this.put('/:name', this.updateTagType, UPDATE_TAG_TYPE);
|
||||||
this.delete('/:name', this.deleteTagType, DELETE_TAG_TYPE);
|
this.delete('/:name', this.deleteTagType, DELETE_TAG_TYPE);
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import Controller from '../controller';
|
import Controller from '../controller';
|
||||||
import { ADMIN } from '../../types/permissions';
|
import { ADMIN, NONE } from '../../types/permissions';
|
||||||
import UserService from '../../services/user-service';
|
import UserService from '../../services/user-service';
|
||||||
import { AccessService } from '../../services/access-service';
|
import { AccessService } from '../../services/access-service';
|
||||||
import { Logger } from '../../logger';
|
import { Logger } from '../../logger';
|
||||||
@ -60,12 +60,12 @@ export default class UserAdminController extends Controller {
|
|||||||
this.get('/', this.getUsers, ADMIN);
|
this.get('/', this.getUsers, ADMIN);
|
||||||
this.get('/search', this.search);
|
this.get('/search', this.search);
|
||||||
this.post('/', this.createUser, ADMIN);
|
this.post('/', this.createUser, ADMIN);
|
||||||
this.post('/validate-password', this.validatePassword);
|
this.post('/validate-password', this.validatePassword, NONE);
|
||||||
this.get('/:id', this.getUser, ADMIN);
|
this.get('/:id', this.getUser, ADMIN);
|
||||||
this.put('/:id', this.updateUser, ADMIN);
|
this.put('/:id', this.updateUser, ADMIN);
|
||||||
this.post('/:id/change-password', this.changePassword, ADMIN);
|
this.post('/:id/change-password', this.changePassword, ADMIN);
|
||||||
this.delete('/:id', this.deleteUser, ADMIN);
|
this.delete('/:id', this.deleteUser, ADMIN);
|
||||||
this.post('/reset-password', this.resetPassword);
|
this.post('/reset-password', this.resetPassword, ADMIN);
|
||||||
this.get('/active-sessions', this.getActiveSessions, ADMIN);
|
this.get('/active-sessions', this.getActiveSessions, ADMIN);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@ import { IUnleashConfig } from '../../types/option';
|
|||||||
import { IUnleashServices } from '../../types/services';
|
import { IUnleashServices } from '../../types/services';
|
||||||
import UserFeedbackService from '../../services/user-feedback-service';
|
import UserFeedbackService from '../../services/user-feedback-service';
|
||||||
import { IAuthRequest } from '../unleash-types';
|
import { IAuthRequest } from '../unleash-types';
|
||||||
|
import { NONE } from '../../types/permissions';
|
||||||
|
|
||||||
interface IFeedbackBody {
|
interface IFeedbackBody {
|
||||||
neverShow?: boolean;
|
neverShow?: boolean;
|
||||||
@ -26,8 +27,8 @@ class UserFeedbackController extends Controller {
|
|||||||
this.logger = config.getLogger('feedback-controller.ts');
|
this.logger = config.getLogger('feedback-controller.ts');
|
||||||
this.userFeedbackService = userFeedbackService;
|
this.userFeedbackService = userFeedbackService;
|
||||||
|
|
||||||
this.post('/', this.recordFeedback);
|
this.post('/', this.recordFeedback, NONE);
|
||||||
this.put('/:id', this.updateFeedbackSettings);
|
this.put('/:id', this.updateFeedbackSettings, NONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async recordFeedback(
|
private async recordFeedback(
|
||||||
|
@ -6,6 +6,7 @@ import { IUnleashConfig } from '../../types/option';
|
|||||||
import { IUnleashServices } from '../../types/services';
|
import { IUnleashServices } from '../../types/services';
|
||||||
import UserSplashService from '../../services/user-splash-service';
|
import UserSplashService from '../../services/user-splash-service';
|
||||||
import { IAuthRequest } from '../unleash-types';
|
import { IAuthRequest } from '../unleash-types';
|
||||||
|
import { NONE } from '../../types/permissions';
|
||||||
|
|
||||||
interface ISplashBody {
|
interface ISplashBody {
|
||||||
seen: boolean;
|
seen: boolean;
|
||||||
@ -25,7 +26,7 @@ class UserSplashController extends Controller {
|
|||||||
this.logger = config.getLogger('splash-controller.ts');
|
this.logger = config.getLogger('splash-controller.ts');
|
||||||
this.userSplashService = userSplashService;
|
this.userSplashService = userSplashService;
|
||||||
|
|
||||||
this.post('/:id', this.updateSplashSettings);
|
this.post('/:id', this.updateSplashSettings, NONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async updateSplashSettings(
|
private async updateSplashSettings(
|
||||||
|
@ -8,6 +8,7 @@ import UserService from '../../services/user-service';
|
|||||||
import SessionService from '../../services/session-service';
|
import SessionService from '../../services/session-service';
|
||||||
import UserFeedbackService from '../../services/user-feedback-service';
|
import UserFeedbackService from '../../services/user-feedback-service';
|
||||||
import UserSplashService from '../../services/user-splash-service';
|
import UserSplashService from '../../services/user-splash-service';
|
||||||
|
import { NONE } from '../../types/permissions';
|
||||||
|
|
||||||
interface IChangeUserRequest {
|
interface IChangeUserRequest {
|
||||||
password: string;
|
password: string;
|
||||||
@ -50,7 +51,7 @@ class UserController extends Controller {
|
|||||||
this.userSplashService = userSplashService;
|
this.userSplashService = userSplashService;
|
||||||
|
|
||||||
this.get('/', this.getUser);
|
this.get('/', this.getUser);
|
||||||
this.post('/change-password', this.updateUserPass);
|
this.post('/change-password', this.updateUserPass, NONE);
|
||||||
this.get('/my-sessions', this.mySessions);
|
this.get('/my-sessions', this.mySessions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ import UserService from '../../services/user-service';
|
|||||||
import { Logger } from '../../logger';
|
import { Logger } from '../../logger';
|
||||||
import { IUnleashConfig } from '../../types/option';
|
import { IUnleashConfig } from '../../types/option';
|
||||||
import { IUnleashServices } from '../../types/services';
|
import { IUnleashServices } from '../../types/services';
|
||||||
|
import { NONE } from '../../types/permissions';
|
||||||
|
|
||||||
interface IValidateQuery {
|
interface IValidateQuery {
|
||||||
token: string;
|
token: string;
|
||||||
@ -31,9 +32,9 @@ class ResetPasswordController extends Controller {
|
|||||||
);
|
);
|
||||||
this.userService = userService;
|
this.userService = userService;
|
||||||
this.get('/validate', this.validateToken);
|
this.get('/validate', this.validateToken);
|
||||||
this.post('/password', this.changePassword);
|
this.post('/password', this.changePassword, NONE);
|
||||||
this.post('/validate-password', this.validatePassword);
|
this.post('/validate-password', this.validatePassword, NONE);
|
||||||
this.post('/password-email', this.sendResetPasswordEmail);
|
this.post('/password-email', this.sendResetPasswordEmail, NONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendResetPasswordEmail(req: Request, res: Response): Promise<void> {
|
async sendResetPasswordEmail(req: Request, res: Response): Promise<void> {
|
||||||
|
@ -10,6 +10,7 @@ import { ALL } from '../../types/models/api-token';
|
|||||||
import ClientMetricsServiceV2 from '../../services/client-metrics/client-metrics-service-v2';
|
import ClientMetricsServiceV2 from '../../services/client-metrics/client-metrics-service-v2';
|
||||||
import { User } from '../../server-impl';
|
import { User } from '../../server-impl';
|
||||||
import { IClientApp } from '../../types/model';
|
import { IClientApp } from '../../types/model';
|
||||||
|
import { NONE } from '../../types/permissions';
|
||||||
|
|
||||||
export default class ClientMetricsController extends Controller {
|
export default class ClientMetricsController extends Controller {
|
||||||
logger: Logger;
|
logger: Logger;
|
||||||
@ -35,7 +36,7 @@ export default class ClientMetricsController extends Controller {
|
|||||||
this.metrics = clientMetricsService;
|
this.metrics = clientMetricsService;
|
||||||
this.metricsV2 = clientMetricsServiceV2;
|
this.metricsV2 = clientMetricsServiceV2;
|
||||||
|
|
||||||
this.post('/', this.registerMetrics);
|
this.post('/', this.registerMetrics, NONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
private resolveEnvironment(user: User, data: IClientApp) {
|
private resolveEnvironment(user: User, data: IClientApp) {
|
||||||
|
@ -8,6 +8,7 @@ import { IAuthRequest, User } from '../../server-impl';
|
|||||||
import { IClientApp } from '../../types/model';
|
import { IClientApp } from '../../types/model';
|
||||||
import ApiUser from '../../types/api-user';
|
import ApiUser from '../../types/api-user';
|
||||||
import { ALL } from '../../types/models/api-token';
|
import { ALL } from '../../types/models/api-token';
|
||||||
|
import { NONE } from '../../types/permissions';
|
||||||
|
|
||||||
export default class RegisterController extends Controller {
|
export default class RegisterController extends Controller {
|
||||||
logger: Logger;
|
logger: Logger;
|
||||||
@ -23,7 +24,9 @@ export default class RegisterController extends Controller {
|
|||||||
super(config);
|
super(config);
|
||||||
this.logger = config.getLogger('/api/client/register');
|
this.logger = config.getLogger('/api/client/register');
|
||||||
this.metrics = clientMetricsService;
|
this.metrics = clientMetricsService;
|
||||||
this.post('/', this.handleRegister);
|
|
||||||
|
// NONE permission is not optimal here in terms of readability.
|
||||||
|
this.post('/', this.handleRegister, NONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
private resolveEnvironment(user: User, data: IClientApp) {
|
private resolveEnvironment(user: User, data: IClientApp) {
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
import { IRouter, Request, Response } from 'express';
|
import { IRouter, Router, Request, Response } from 'express';
|
||||||
import { Logger } from 'lib/logger';
|
import { Logger } from 'lib/logger';
|
||||||
import { IUnleashConfig } from '../types/option';
|
import { IUnleashConfig } from '../types/option';
|
||||||
|
import { NONE } from '../types/permissions';
|
||||||
import { handleErrors } from './util';
|
import { handleErrors } from './util';
|
||||||
|
import NoAccessError from '../error/no-access-error';
|
||||||
const { Router } = require('express');
|
import requireContentType from '../middleware/content_type_checker';
|
||||||
const NoAccessError = require('../error/no-access-error');
|
|
||||||
const requireContentType = require('../middleware/content_type_checker');
|
|
||||||
|
|
||||||
interface IRequestHandler<
|
interface IRequestHandler<
|
||||||
P = any,
|
P = any,
|
||||||
@ -20,7 +19,7 @@ interface IRequestHandler<
|
|||||||
}
|
}
|
||||||
|
|
||||||
const checkPermission = (permission) => async (req, res, next) => {
|
const checkPermission = (permission) => async (req, res, next) => {
|
||||||
if (!permission) {
|
if (!permission || permission === NONE) {
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
if (req.checkRbac && (await req.checkRbac(permission))) {
|
if (req.checkRbac && (await req.checkRbac(permission))) {
|
||||||
@ -73,7 +72,7 @@ export default class Controller {
|
|||||||
post(
|
post(
|
||||||
path: string,
|
path: string,
|
||||||
handler: IRequestHandler,
|
handler: IRequestHandler,
|
||||||
permission?: string,
|
permission: string,
|
||||||
...acceptedContentTypes: string[]
|
...acceptedContentTypes: string[]
|
||||||
): void {
|
): void {
|
||||||
this.app.post(
|
this.app.post(
|
||||||
@ -87,7 +86,7 @@ export default class Controller {
|
|||||||
put(
|
put(
|
||||||
path: string,
|
path: string,
|
||||||
handler: IRequestHandler,
|
handler: IRequestHandler,
|
||||||
permission?: string,
|
permission: string,
|
||||||
...acceptedContentTypes: string[]
|
...acceptedContentTypes: string[]
|
||||||
): void {
|
): void {
|
||||||
this.app.put(
|
this.app.put(
|
||||||
@ -101,7 +100,7 @@ export default class Controller {
|
|||||||
patch(
|
patch(
|
||||||
path: string,
|
path: string,
|
||||||
handler: IRequestHandler,
|
handler: IRequestHandler,
|
||||||
permission?: string,
|
permission: string,
|
||||||
...acceptedContentTypes: string[]
|
...acceptedContentTypes: string[]
|
||||||
): void {
|
): void {
|
||||||
this.app.patch(
|
this.app.patch(
|
||||||
@ -112,7 +111,7 @@ export default class Controller {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
delete(path: string, handler: IRequestHandler, permission?: string): void {
|
delete(path: string, handler: IRequestHandler, permission: string): void {
|
||||||
this.app.delete(
|
this.app.delete(
|
||||||
path,
|
path,
|
||||||
checkPermission(permission),
|
checkPermission(permission),
|
||||||
@ -124,7 +123,7 @@ export default class Controller {
|
|||||||
path: string,
|
path: string,
|
||||||
filehandler: IRequestHandler,
|
filehandler: IRequestHandler,
|
||||||
handler: Function,
|
handler: Function,
|
||||||
permission?: string,
|
permission: string,
|
||||||
): void {
|
): void {
|
||||||
this.app.post(
|
this.app.post(
|
||||||
path,
|
path,
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
|
// Special
|
||||||
export const ADMIN = 'ADMIN';
|
export const ADMIN = 'ADMIN';
|
||||||
export const CLIENT = 'CLIENT';
|
export const CLIENT = 'CLIENT';
|
||||||
|
export const NONE = 'NONE';
|
||||||
|
|
||||||
export const CREATE_FEATURE = 'CREATE_FEATURE';
|
export const CREATE_FEATURE = 'CREATE_FEATURE';
|
||||||
export const UPDATE_FEATURE = 'UPDATE_FEATURE';
|
export const UPDATE_FEATURE = 'UPDATE_FEATURE';
|
||||||
export const DELETE_FEATURE = 'DELETE_FEATURE';
|
export const DELETE_FEATURE = 'DELETE_FEATURE';
|
||||||
|
78
src/test/e2e/api/admin/project/features.auth.e2e.test.ts
Normal file
78
src/test/e2e/api/admin/project/features.auth.e2e.test.ts
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import dbInit, { ITestDb } from '../../../helpers/database-init';
|
||||||
|
import { IUnleashTest, setupAppWithAuth } from '../../../helpers/test-helper';
|
||||||
|
import getLogger from '../../../../fixtures/no-logger';
|
||||||
|
import { DEFAULT_ENV } from '../../../../../lib/util/constants';
|
||||||
|
import { RoleName } from '../../../../../lib/server-impl';
|
||||||
|
|
||||||
|
let app: IUnleashTest;
|
||||||
|
let db: ITestDb;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
db = await dbInit('feature_strategy_auth_api_serial', getLogger);
|
||||||
|
app = await setupAppWithAuth(db.stores);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
const all = await db.stores.projectStore.getEnvironmentsForProject(
|
||||||
|
'default',
|
||||||
|
);
|
||||||
|
await Promise.all(
|
||||||
|
all
|
||||||
|
.filter((env) => env !== DEFAULT_ENV)
|
||||||
|
.map(async (env) =>
|
||||||
|
db.stores.projectStore.deleteEnvironmentForProject(
|
||||||
|
'default',
|
||||||
|
env,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await app.destroy();
|
||||||
|
await db.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Should not be possible to update feature toggle without permission', async () => {
|
||||||
|
const email = 'user@mail.com';
|
||||||
|
const url = '/api/admin/projects/default/features';
|
||||||
|
const name = 'auth.toggle.update';
|
||||||
|
|
||||||
|
await db.stores.featureToggleStore.create('default', { name });
|
||||||
|
|
||||||
|
await app.services.userService.createUser({
|
||||||
|
email,
|
||||||
|
rootRole: RoleName.VIEWER,
|
||||||
|
});
|
||||||
|
|
||||||
|
await app.request.post('/auth/demo/login').send({
|
||||||
|
email,
|
||||||
|
});
|
||||||
|
|
||||||
|
await app.request
|
||||||
|
.put(`${url}/${name}`)
|
||||||
|
.send({ name, description: 'updated', type: 'kill-switch' })
|
||||||
|
.expect(403);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Should be possible to update feature toggle with permission', async () => {
|
||||||
|
const email = 'user2@mail.com';
|
||||||
|
const url = '/api/admin/projects/default/features';
|
||||||
|
const name = 'auth.toggle.update2';
|
||||||
|
|
||||||
|
await db.stores.featureToggleStore.create('default', { name });
|
||||||
|
|
||||||
|
await app.services.userService.createUser({
|
||||||
|
email,
|
||||||
|
rootRole: RoleName.EDITOR,
|
||||||
|
});
|
||||||
|
|
||||||
|
await app.request.post('/auth/demo/login').send({
|
||||||
|
email,
|
||||||
|
});
|
||||||
|
|
||||||
|
await app.request
|
||||||
|
.put(`${url}/${name}`)
|
||||||
|
.send({ name, description: 'updated', type: 'kill-switch' })
|
||||||
|
.expect(200);
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user