]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Add support for URI encode 22672/head
authorTiago Melo <tmelo@suse.com>
Fri, 22 Jun 2018 14:14:39 +0000 (15:14 +0100)
committerTiago Melo <tmelo@suse.com>
Tue, 26 Jun 2018 13:35:15 +0000 (14:35 +0100)
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 <tmelo@suse.com>
18 files changed:
qa/tasks/mgr/dashboard/test_rbd.py
src/pybind/mgr/dashboard/controllers/__init__.py
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.html
src/pybind/mgr/dashboard/frontend/src/app/shared/api/pool.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/rbd.service.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/rbd.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-bucket.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-daemon.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-user.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/decorators/cd-encode.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/encode-uri.pipe.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/encode-uri.pipe.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/pipes.module.ts

index e00682970354d336476d731aa3c5cc9c09bd6bce..aef3f1e07c8a33c6e80b50e9bf7f4581df31b5d5 100644 (file)
@@ -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)
index c9016d47dfbf1986d15bf8a088e564eafd7910d2..9c782f51e96aab1864748365cd34c80cf99b25a5 100644 (file)
@@ -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)
 
index b980986a426f5704c81acb5e5d2f4e3235d946aa..391b3204cb5f9758f25b17c7d7e819d700e2f0dc 100644 (file)
@@ -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);
         });
index 0d2f37e5b535b9c3e8a3bb1c3bffcbf14490e22e..5d915a49feac1d398f6bb25e1a16ec3d48f6a7fc 100644 (file)
@@ -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 }}">
-        <i class="fa fa-fw fa-pencil"></i><span i18n>Edit</span>
+              routerLink="/rbd/edit/{{ selection.first()?.pool_name | encodeUri }}/{{ selection.first()?.name | encodeUri }}">
+        <i class="fa fa-fw fa-pencil"></i>
+        <span i18n>Edit</span>
       </button>
       <button type="button"
               class="btn btn-sm btn-primary"
@@ -64,7 +65,7 @@
             *ngIf="permission.update"
             [ngClass]="{'disabled': !selection.hasSingleSelection || selection.first().executing}">
           <a class="dropdown-item"
-             routerLink="/rbd/edit/{{ selection.first()?.pool_name }}/{{ selection.first()?.name }}">
+             routerLink="/rbd/edit/{{ selection.first()?.pool_name | encodeUri }}/{{ selection.first()?.name | encodeUri }}">
             <i class="fa fa-fw fa-pencil"></i>
             <span i18n>Edit</span>
           </a>
@@ -73,7 +74,7 @@
             *ngIf="permission.create"
             [ngClass]="{'disabled': !selection.hasSingleSelection || selection.first().executing}">
           <a class="dropdown-item"
-             routerLink="/rbd/copy/{{ selection.first()?.pool_name }}/{{ selection.first()?.name }}">
+             routerLink="/rbd/copy/{{ selection.first()?.pool_name | encodeUri }}/{{ selection.first()?.name | encodeUri }}">
             <i class="fa fa-fw fa-copy"></i>
             <span i18n>Copy</span>
           </a>
index 1e6ed5fc98f98d9efd728753eaafd046786b74ab..a742f216944739fd852f9e3a0656add9e9520668 100644 (file)
@@ -73,7 +73,7 @@
             *ngIf="permission.create"
             [ngClass]="{'disabled': !selection.hasSingleSelection || selection.first().executing}">
           <a class="dropdown-item"
-             routerLink="/rbd/clone/{{ poolName }}/{{ rbdName }}/{{ selection.first()?.name }}">
+             routerLink="/rbd/clone/{{ poolName | encodeUri }}/{{ rbdName | encodeUri }}/{{ selection.first()?.name | encodeUri }}">
             <i class="fa fa-fw fa-clone"></i>
             <span i18n>Clone</span>
           </a>
         <li role="menuitem"
             *ngIf="permission.create"
             [ngClass]="{'disabled': !selection.hasSingleSelection || selection.first().executing}">
-          <a class="dropdown-item" routerLink="/rbd/copy/{{ poolName }}/{{ rbdName }}/{{ selection.first()?.name }}">
-            <i class="fa fa-fw fa-copy"></i><span i18n>Copy</span>
+          <a class="dropdown-item"
+             routerLink="/rbd/copy/{{ poolName | encodeUri }}/{{ rbdName | encodeUri }}/{{ selection.first()?.name | encodeUri }}">
+            <i class="fa fa-fw fa-copy"></i>
+            <span i18n>Copy</span>
           </a>
         </li>
         <li role="menuitem"
index 03fe0a050f3dff6d5be0f32625caa671b4eff26b..e709b4e4c5469fbae9aca2017fb7e18eda3bed2b 100644 (file)
@@ -11,6 +11,7 @@ import { RbdService } from '../../../shared/api/rbd.service';
 import { ComponentsModule } from '../../../shared/components/components.module';
 import { DataTableModule } from '../../../shared/datatable/datatable.module';
 import { Permissions } from '../../../shared/models/permissions';
+import { PipesModule } from '../../../shared/pipes/pipes.module';
 import { AuthStorageService } from '../../../shared/services/auth-storage.service';
 import { NotificationService } from '../../../shared/services/notification.service';
 import { ServicesModule } from '../../../shared/services/services.module';
@@ -40,7 +41,8 @@ describe('RbdSnapshotListComponent', () => {
       ServicesModule,
       ApiModule,
       HttpClientTestingModule,
-      RouterTestingModule
+      RouterTestingModule,
+      PipesModule
     ],
     providers: [{ provide: AuthStorageService, useValue: fakeAuthStorageService }]
   });
index 6710cb1c4ab7062d2d928991655ae6fa3f7c9c57..7801f3720d685943a50f6a7308c4aff1b3a36276 100644 (file)
@@ -56,6 +56,7 @@ export class RgwBucketFormComponent implements OnInit {
         if (!params.hasOwnProperty('bucket')) {
           return;
         }
+        params.bucket = decodeURIComponent(params.bucket);
         this.loading = true;
         // Load the bucket data in 'edit' mode.
         this.editing = true;
index c1024dd74be3f5a51fdebbfd8dfa6d1b28badd04..fd5388573844a7dd143f29cef9480b4642a4d475 100644 (file)
@@ -34,7 +34,7 @@
               class="btn btn-sm btn-primary"
               [ngClass]="{'disabled': !selection.hasSelection}"
               *ngIf="permission.update && (!permission.create && !selection.hasMultiSelection || selection.hasSingleSelection)"
-              routerLink="/rgw/bucket/edit/{{ selection.first()?.bucket }}">
+              routerLink="/rgw/bucket/edit/{{ selection.first()?.bucket | encodeUri }}">
         <i class="fa fa-fw fa-pencil"></i>
         <ng-container i18n>Edit</ng-container>
       </button>
@@ -69,7 +69,7 @@
             *ngIf="permission.update"
             [ngClass]="{'disabled': !selection.hasSingleSelection}">
           <a class="dropdown-item"
-             routerLink="/rgw/bucket/edit/{{ selection.first()?.bucket }}"
+             routerLink="/rgw/bucket/edit/{{ selection.first()?.bucket | encodeUri }}"
              i18n>
             <i class="fa fa-fw fa-pencil"></i>
             Edit
index 4d5a90eb1bfcf2b69d8c823358da7f547f54d2a3..32cc67bdb4cbca0cf50b77d8d0e15cd8ee9c713d 100644 (file)
@@ -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
 })
index 4d311c86a908f38b5dd226b28c4e4ea64fe770aa..09d11a5b6613000a7a4955cba14f94ae20679139 100644 (file)
@@ -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');
+    });
+  });
 });
index a22362ef35935cdd9dbe7eb9c16f6a0a0238d1c7..9a466adf9161a2b2b56cf564335e6b40835fcb85 100644 (file)
@@ -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
 })
index 8d16e0ecf739ef8672bd04185bf76461bcf1ccec..f1de82eda5a8338214136ff656a499f5ae30bd48 100644 (file)
@@ -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
 })
index 59b83dc588f8260facad5abf74f43f1c4d6eac9c..9dde8ea75e0dbc9cdd73d08f939242b6ca502501 100644 (file)
@@ -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
 })
index e93e2102d3fbf690428253557af6f4941082f082..a1563940737e8563401b31edd1bf2ad95566b65a 100644 (file)
@@ -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 (file)
index 0000000..c2e4f9f
--- /dev/null
@@ -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 (file)
index 0000000..a436740
--- /dev/null
@@ -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 (file)
index 0000000..48fbf16
--- /dev/null
@@ -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);
+  }
+}
index 1aba100b7629762ede33d1205fcb0ea0ed866a7d..fb916b712cc2d49f4c487d8fbc7dbb4067b561b4 100644 (file)
@@ -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 {}