From f21d0da5a3e19e3f9bbde084e71b4a09f8dcb0a1 Mon Sep 17 00:00:00 2001 From: Tiago Melo Date: Fri, 22 Jun 2018 15:14:39 +0100 Subject: [PATCH] mgr/dashboard: Add support for URI encode Created a decorator and pipe to help encode special URI components in the frontend. Modified the backend request handler to decode all the string args. fixes: http://tracker.ceph.com/issues/24621 Signed-off-by: Tiago Melo --- qa/tasks/mgr/dashboard/test_rbd.py | 19 ++++++ .../mgr/dashboard/controllers/__init__.py | 14 +++- .../ceph/block/rbd-form/rbd-form.component.ts | 6 +- .../block/rbd-list/rbd-list.component.html | 9 +-- .../rbd-snapshot-list.component.html | 8 ++- .../rbd-snapshot-list.component.spec.ts | 4 +- .../rgw-bucket-form.component.ts | 1 + .../rgw-bucket-list.component.html | 4 +- .../src/app/shared/api/pool.service.ts | 2 + .../src/app/shared/api/rbd.service.spec.ts | 8 +++ .../src/app/shared/api/rbd.service.ts | 2 + .../src/app/shared/api/rgw-bucket.service.ts | 2 + .../src/app/shared/api/rgw-daemon.service.ts | 2 + .../src/app/shared/api/rgw-user.service.ts | 2 + .../src/app/shared/decorators/cd-encode.ts | 66 +++++++++++++++++++ .../app/shared/pipes/encode-uri.pipe.spec.ts | 13 ++++ .../src/app/shared/pipes/encode-uri.pipe.ts | 10 +++ .../src/app/shared/pipes/pipes.module.ts | 10 ++- 18 files changed, 165 insertions(+), 17 deletions(-) create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/decorators/cd-encode.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/encode-uri.pipe.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/encode-uri.pipe.ts diff --git a/qa/tasks/mgr/dashboard/test_rbd.py b/qa/tasks/mgr/dashboard/test_rbd.py index e00682970354d..aef3f1e07c8a3 100644 --- a/qa/tasks/mgr/dashboard/test_rbd.py +++ b/qa/tasks/mgr/dashboard/test_rbd.py @@ -580,3 +580,22 @@ class RbdTest(DashboardTestCase): self.assertEqual(default_features, ['deep-flatten', 'exclusive-lock', 'fast-diff', 'layering', 'object-map']) + + def test_image_with_special_name(self): + rbd_name = 'test/rbd' + rbd_name_encoded = 'test%2Frbd' + + self.create_image('rbd', rbd_name, 10240) + self.assertStatus(201) + + img = self._get("/api/block/image/rbd/" + rbd_name_encoded) + self.assertStatus(200) + + self._validate_image(img, name=rbd_name, size=10240, + num_objs=1, obj_size=4194304, + features_name=['deep-flatten', + 'exclusive-lock', + 'fast-diff', 'layering', + 'object-map']) + + self.remove_image('rbd', rbd_name_encoded) diff --git a/src/pybind/mgr/dashboard/controllers/__init__.py b/src/pybind/mgr/dashboard/controllers/__init__.py index c9016d47dfbf1..9c782f51e96aa 100644 --- a/src/pybind/mgr/dashboard/controllers/__init__.py +++ b/src/pybind/mgr/dashboard/controllers/__init__.py @@ -9,9 +9,15 @@ import json import os import pkgutil import sys +from six import add_metaclass + +if sys.version_info >= (3, 0): + from urllib.parse import unquote # pylint: disable=no-name-in-module,import-error +else: + from urllib import unquote # pylint: disable=no-name-in-module +# pylint: disable=wrong-import-position import cherrypy -from six import add_metaclass from .. import logger from ..security import Scope, Permission @@ -521,6 +527,12 @@ class BaseController(object): def _request_wrapper(func, method, json_response): @wraps(func) def inner(*args, **kwargs): + for key, value in kwargs.items(): + # pylint: disable=undefined-variable + if (sys.version_info < (3, 0) and isinstance(value, unicode)) \ + or isinstance(value, str): + kwargs[key] = unquote(value) + if method in ['GET', 'DELETE']: ret = func(*args, **kwargs) diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.ts index b980986a426f5..391b3204cb5f9 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.ts @@ -206,9 +206,9 @@ export class RbdFormComponent implements OnInit { this.mode === this.rbdFormMode.copying ) { this.route.params.subscribe((params: { pool: string; name: string; snap: string }) => { - const poolName = params.pool; - const rbdName = params.name; - this.snapName = params.snap; + const poolName = decodeURIComponent(params.pool); + const rbdName = decodeURIComponent(params.name); + this.snapName = decodeURIComponent(params.snap); this.rbdService.get(poolName, rbdName).subscribe((resp: RbdFormResponseModel) => { this.setResponse(resp, this.snapName); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.html index 0d2f37e5b535b..5d915a49feac1 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.html @@ -32,8 +32,9 @@ class="btn btn-sm btn-primary" *ngIf="permission.update && (!permission.create || permission.create && selection.hasSingleSelection)" [ngClass]="{'disabled': !selection.hasSingleSelection || selection.first().executing}" - routerLink="/rbd/edit/{{ selection.first()?.pool_name }}/{{ selection.first()?.name }}"> - Edit + routerLink="/rbd/edit/{{ selection.first()?.pool_name | encodeUri }}/{{ selection.first()?.name | encodeUri }}"> + + Edit @@ -69,7 +69,7 @@ *ngIf="permission.update" [ngClass]="{'disabled': !selection.hasSingleSelection}"> Edit diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/pool.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/pool.service.ts index 4d5a90eb1bfcf..32cc67bdb4cbc 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/pool.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/pool.service.ts @@ -1,8 +1,10 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; +import { cdEncode } from '../decorators/cd-encode'; import { ApiModule } from './api.module'; +@cdEncode @Injectable({ providedIn: ApiModule }) diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rbd.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rbd.service.spec.ts index 4d311c86a908f..09d11a5b66130 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rbd.service.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rbd.service.spec.ts @@ -126,4 +126,12 @@ describe('RbdService', () => { const req = httpTesting.expectOne('api/block/image/poolName/rbdName/snap/snapshotName'); expect(req.request.method).toBe('DELETE'); }); + + describe('Encode decorator', () => { + it('should encode the imageName', () => { + service.get('poolName', 'rbd/name').subscribe(); + const req = httpTesting.expectOne('api/block/image/poolName/rbd%2Fname'); + expect(req.request.method).toBe('GET'); + }); + }); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rbd.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rbd.service.ts index a22362ef35935..9a466adf9161a 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rbd.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rbd.service.ts @@ -1,8 +1,10 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; +import { cdEncode } from '../decorators/cd-encode'; import { ApiModule } from './api.module'; +@cdEncode @Injectable({ providedIn: ApiModule }) diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-bucket.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-bucket.service.ts index 8d16e0ecf739e..f1de82eda5a83 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-bucket.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-bucket.service.ts @@ -5,8 +5,10 @@ import * as _ from 'lodash'; import { forkJoin as observableForkJoin, of as observableOf } from 'rxjs'; import { mergeMap } from 'rxjs/operators'; +import { cdEncode } from '../decorators/cd-encode'; import { ApiModule } from './api.module'; +@cdEncode @Injectable({ providedIn: ApiModule }) diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-daemon.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-daemon.service.ts index 59b83dc588f82..9dde8ea75e0db 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-daemon.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-daemon.service.ts @@ -1,8 +1,10 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; +import { cdEncode } from '../decorators/cd-encode'; import { ApiModule } from './api.module'; +@cdEncode @Injectable({ providedIn: ApiModule }) diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-user.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-user.service.ts index e93e2102d3fbf..a1563940737e8 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-user.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-user.service.ts @@ -5,8 +5,10 @@ import * as _ from 'lodash'; import { forkJoin as observableForkJoin, of as observableOf } from 'rxjs'; import { mergeMap } from 'rxjs/operators'; +import { cdEncode } from '../decorators/cd-encode'; import { ApiModule } from './api.module'; +@cdEncode @Injectable({ providedIn: ApiModule }) diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/decorators/cd-encode.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/decorators/cd-encode.ts new file mode 100644 index 0000000000000..c2e4f9f1c286d --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/decorators/cd-encode.ts @@ -0,0 +1,66 @@ +import * as _ from 'lodash'; + +/** + * This decorator can be used in a class or method. + * It will encode all the string parameters of all the methods of a class + * or, if applied on a method, the specified method. + * + * @export + * @param {Function} [target=null] + * @returns {*} + */ +export function cdEncode(target: Function = null): any { + if (target) { + encodeClass(target); + } else { + return encodeMethod(); + } +} + +function encodeClass(target: Function) { + for (const propertyName of Object.keys(target.prototype)) { + const descriptor = Object.getOwnPropertyDescriptor(target.prototype, propertyName); + const isMethod = descriptor.value instanceof Function; + if (!isMethod) { + continue; + } + + const originalMethod = descriptor.value; + descriptor.value = function(...args: any[]) { + args.forEach((arg, i, argsArray) => { + if (_.isString(arg)) { + argsArray[i] = encodeURIComponent(arg); + } + }); + + const result = originalMethod.apply(this, args); + return result; + }; + + Object.defineProperty(target.prototype, propertyName, descriptor); + } +} + +function encodeMethod() { + return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) { + if (descriptor === undefined) { + descriptor = Object.getOwnPropertyDescriptor(target, propertyKey); + } + const originalMethod = descriptor.value; + + descriptor.value = function() { + const args = []; + + for (let i = 0; i < arguments.length; i++) { + if (_.isString(arguments[i])) { + args[i] = encodeURIComponent(arguments[i]); + } else { + args[i] = arguments[i]; + } + } + + const result = originalMethod.apply(this, args); + return result; + }; + }; +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/encode-uri.pipe.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/encode-uri.pipe.spec.ts new file mode 100644 index 0000000000000..a436740930d02 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/encode-uri.pipe.spec.ts @@ -0,0 +1,13 @@ +import { EncodeUriPipe } from './encode-uri.pipe'; + +describe('EncodeUriPipe', () => { + it('create an instance', () => { + const pipe = new EncodeUriPipe(); + expect(pipe).toBeTruthy(); + }); + + it('should transforms the value', () => { + const pipe = new EncodeUriPipe(); + expect(pipe.transform('rbd/name')).toBe('rbd%2Fname'); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/encode-uri.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/encode-uri.pipe.ts new file mode 100644 index 0000000000000..48fbf16683ca9 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/encode-uri.pipe.ts @@ -0,0 +1,10 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'encodeUri' +}) +export class EncodeUriPipe implements PipeTransform { + transform(value: any): any { + return encodeURIComponent(value); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/pipes.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/pipes.module.ts index 1aba100b76297..fb916b712cc2d 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/pipes.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/pipes.module.ts @@ -7,6 +7,7 @@ import { CephReleaseNamePipe } from './ceph-release-name.pipe'; import { CephShortVersionPipe } from './ceph-short-version.pipe'; import { DimlessBinaryPipe } from './dimless-binary.pipe'; import { DimlessPipe } from './dimless.pipe'; +import { EncodeUriPipe } from './encode-uri.pipe'; import { FilterPipe } from './filter.pipe'; import { HealthColorPipe } from './health-color.pipe'; import { ListPipe } from './list.pipe'; @@ -24,7 +25,8 @@ import { RelativeDatePipe } from './relative-date.pipe'; ListPipe, FilterPipe, CdDatePipe, - EmptyPipe + EmptyPipe, + EncodeUriPipe ], exports: [ DimlessBinaryPipe, @@ -36,7 +38,8 @@ import { RelativeDatePipe } from './relative-date.pipe'; ListPipe, FilterPipe, CdDatePipe, - EmptyPipe + EmptyPipe, + EncodeUriPipe ], providers: [ DatePipe, @@ -47,7 +50,8 @@ import { RelativeDatePipe } from './relative-date.pipe'; RelativeDatePipe, ListPipe, CdDatePipe, - EmptyPipe + EmptyPipe, + EncodeUriPipe ] }) export class PipesModule {} -- 2.39.5