]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: add server side encryption to rgw/s3 48441/head
authorAashish Sharma <aasharma@redhat.com>
Mon, 8 Aug 2022 12:35:02 +0000 (18:05 +0530)
committerAashish Sharma <aasharma@redhat.com>
Tue, 11 Oct 2022 11:00:00 +0000 (16:30 +0530)
Fixes:https://tracker.ceph.com/issues/57826
Signed-off-by: Aashish Sharma <aasharma@redhat.com>
(cherry picked from commit 42aa2283c2aea6059586cf3bb213a127d8879a45)

16 files changed:
src/pybind/mgr/dashboard/controllers/rgw.py
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-bucket-encryption.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-details/rgw-bucket-details.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.html
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-config-modal/rgw-config-modal.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-config-modal/rgw-config-modal.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-config-modal/rgw-config-modal.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-config-modal/rgw-config-modal.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-bucket.service.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-bucket.service.ts
src/pybind/mgr/dashboard/openapi.yaml
src/pybind/mgr/dashboard/services/ceph_service.py
src/pybind/mgr/dashboard/services/rgw_client.py
src/pybind/mgr/mgr_util.py

index f42b91a0e940e6343fcc26b36ea71ea7d12d11a9..104a5a40d35fe98dbf1ec33a9285e7581773d8db 100644 (file)
@@ -197,6 +197,28 @@ class RgwBucket(RgwRESTController):
             rgw_client.set_bucket_versioning(bucket_name, versioning_state, mfa_delete,
                                              mfa_token_serial, mfa_token_pin)
 
+    def _set_encryption(self, bid, encryption_type, key_id, daemon_name, owner):
+
+        rgw_client = RgwClient.instance(owner, daemon_name)
+        rgw_client.set_bucket_encryption(bid, key_id, encryption_type)
+
+    # pylint: disable=W0613
+    def _set_encryption_config(self, encryption_type, kms_provider, auth_method, secret_engine,
+                               secret_path, namespace, address, token, daemon_name, owner,
+                               ssl_cert, client_cert, client_key):
+
+        CephService.set_encryption_config(encryption_type, kms_provider, auth_method,
+                                          secret_engine, secret_path, namespace, address,
+                                          token, ssl_cert, client_cert, client_key)
+
+    def _get_encryption(self, bucket_name, daemon_name, owner):
+        rgw_client = RgwClient.instance(owner, daemon_name)
+        return rgw_client.get_bucket_encryption(bucket_name)
+
+    def _delete_encryption(self, bucket_name, daemon_name, owner):
+        rgw_client = RgwClient.instance(owner, daemon_name)
+        return rgw_client.delete_bucket_encryption(bucket_name)
+
     def _get_locking(self, owner, daemon_name, bucket_name):
         rgw_client = RgwClient.instance(owner, daemon_name)
         return rgw_client.get_bucket_locking(bucket_name)
@@ -256,6 +278,8 @@ class RgwBucket(RgwRESTController):
 
         # Append the versioning configuration.
         versioning = self._get_versioning(result['owner'], daemon_name, bucket_name)
+        encryption = self._get_encryption(bucket_name, daemon_name, result['owner'])
+        result['encryption'] = encryption['Status']
         result['versioning'] = versioning['Status']
         result['mfa_delete'] = versioning['MfaDelete']
 
@@ -269,8 +293,10 @@ class RgwBucket(RgwRESTController):
     def create(self, bucket, uid, zonegroup=None, placement_target=None,
                lock_enabled='false', lock_mode=None,
                lock_retention_period_days=None,
-               lock_retention_period_years=None, daemon_name=None):
+               lock_retention_period_years=None, encryption_state='false',
+               encryption_type=None, key_id=None, daemon_name=None):
         lock_enabled = str_to_bool(lock_enabled)
+        encryption_state = str_to_bool(encryption_state)
         try:
             rgw_client = RgwClient.instance(uid, daemon_name)
             result = rgw_client.create_bucket(bucket, zonegroup,
@@ -280,15 +306,21 @@ class RgwBucket(RgwRESTController):
                 self._set_locking(uid, daemon_name, bucket, lock_mode,
                                   lock_retention_period_days,
                                   lock_retention_period_years)
+
+            if encryption_state:
+                self._set_encryption(bucket, encryption_type, key_id, daemon_name, uid)
+
             return result
         except RequestException as e:  # pragma: no cover - handling is too obvious
             raise DashboardException(e, http_status_code=500, component='rgw')
 
     @allow_empty_body
     def set(self, bucket, bucket_id, uid, versioning_state=None,
+            encryption_state='false', encryption_type=None, key_id=None,
             mfa_delete=None, mfa_token_serial=None, mfa_token_pin=None,
             lock_mode=None, lock_retention_period_days=None,
             lock_retention_period_years=None, daemon_name=None):
+        encryption_state = str_to_bool(encryption_state)
         # When linking a non-tenant-user owned bucket to a tenanted user, we
         # need to prefix bucket name with '/'. e.g. photos -> /photos
         if '$' in uid and '/' not in bucket:
@@ -322,6 +354,11 @@ class RgwBucket(RgwRESTController):
                               lock_retention_period_days,
                               lock_retention_period_years)
 
+        encryption_status = self._get_encryption(bucket_name, daemon_name, uid)
+        if encryption_state and encryption_status['Status'] != 'Enabled':
+            self._set_encryption(bucket_name, encryption_type, key_id, daemon_name, uid)
+        if encryption_status['Status'] == 'Enabled' and (not encryption_state):
+            self._delete_encryption(bucket_name, daemon_name, uid)
         return self._append_bid(result)
 
     def delete(self, bucket, purge_objects='true', daemon_name=None):
@@ -330,6 +367,32 @@ class RgwBucket(RgwRESTController):
             'purge-objects': purge_objects
         }, json_response=False)
 
+    @RESTController.Collection(method='PUT', path='/setEncryptionConfig')
+    @allow_empty_body
+    def set_encryption_config(self, encryption_type=None, kms_provider=None, auth_method=None,
+                              secret_engine=None, secret_path='', namespace='', address=None,
+                              token=None, daemon_name=None, owner=None, ssl_cert=None,
+                              client_cert=None, client_key=None):
+        return self._set_encryption_config(encryption_type, kms_provider, auth_method,
+                                           secret_engine, secret_path, namespace,
+                                           address, token, daemon_name, owner, ssl_cert,
+                                           client_cert, client_key)
+
+    @RESTController.Collection(method='GET', path='/getEncryption')
+    @allow_empty_body
+    def get_encryption(self, bucket_name, daemon_name=None, owner=None):
+        return self._get_encryption(bucket_name, daemon_name, owner)
+
+    @RESTController.Collection(method='DELETE', path='/deleteEncryption')
+    @allow_empty_body
+    def delete_encryption(self, bucket_name, daemon_name=None, owner=None):
+        return self._delete_encryption(bucket_name, daemon_name, owner)
+
+    @RESTController.Collection(method='GET', path='/getEncryptionConfig')
+    @allow_empty_body
+    def get_encryption_config(self):
+        return CephService.get_encryption_config()
+
 
 @APIRouter('/rgw/user', Scope.RGW)
 @APIDoc("RGW User Management API", "RgwUser")
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-bucket-encryption.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-bucket-encryption.ts
new file mode 100644 (file)
index 0000000..e4f81f6
--- /dev/null
@@ -0,0 +1,7 @@
+export class RgwBucketEncryptionModel {
+  kmsProviders = ['vault'];
+  authMethods = ['token', 'agent'];
+  secretEngines = ['kv', 'transit'];
+  sse_s3 = 'AES256';
+  sse_kms = 'aws:kms';
+}
index bf4bdcb08d959227b05ed522272980bfc1942d1c..15b33e141229d47c0bc1d7587498420712704adc 100644 (file)
             class="bold">Versioning</td>
         <td>{{ selection.versioning }}</td>
       </tr>
+      <tr>
+        <td i18n
+            class="bold">Encryption</td>
+        <td>{{ selection.encryption }}</td>
+      </tr>
       <tr>
         <td i18n
             class="bold">MFA Delete</td>
index bad80a7f358874f69357e14812bb03b29c78bd85..4d2bc5ff2e12d05f75727d21ab270e9bc67386ac 100644 (file)
           </div>
         </fieldset>
 
+        <fieldset>
+          <legend class="cd-header"
+                  i18n>Security</legend>
+          <div class="form-group row">
+            <div class="cd-col-form-offset">
+              <div class="custom-control custom-checkbox">
+                <input class="custom-control-input"
+                       id="encryption_enabled"
+                       name="encryption_enabled"
+                       formControlName="encryption_enabled"
+                       type="checkbox"
+                       [attr.disabled]="!kmsVaultConfig && !s3VaultConfig ? true : null">
+                <label class="custom-control-label"
+                       for="encryption_enabled"
+                       i18n>Encryption</label>
+                <cd-helper>
+                  <span i18n>Enables encryption for the objects in the bucket.
+                     To enable encryption on a bucket you need to set the configuration values for SSE-S3 or SSE-KMS.
+                     To set the configuration values <a href="#/rgw/bucket/create"
+                                                        (click)="openConfigModal()">Click here</a></span>
+                </cd-helper>
+              </div>
+            </div>
+          </div>
+
+          <div *ngIf="bucketForm.getValue('encryption_enabled')">
+            <div class="form-group row">
+              <div class="cd-col-form-offset">
+                <div class="custom-control custom-radio custom-control-inline pl-5">
+                  <input class="custom-control-input"
+                         formControlName="encryption_type"
+                         id="sse_S3_enabled"
+                         type="radio"
+                         name="encryption_type"
+                         value="AES256"
+                         [attr.disabled]="!s3VaultConfig ? true : null">
+                  <label class="custom-control-label"
+                         for="sse_S3_enabled"
+                         i18n>SSE-S3 Encryption</label>
+                </div>
+              </div>
+            </div>
+
+            <div class="form-group row">
+              <div class="cd-col-form-offset">
+                <div class="custom-control custom-radio custom-control-inline pl-5">
+                  <input class="custom-control-input"
+                         formControlName="encryption_type"
+                         id="kms_enabled"
+                         name="encryption_type"
+                         value="aws:kms"
+                         [attr.disabled]="!kmsVaultConfig ? true : null"
+                         type="radio">
+                  <label class="custom-control-label"
+                         for="kms_enabled"
+                         i18n>Connect to an external key management service</label>
+                </div>
+              </div>
+            </div>
+
+            <div *ngIf="bucketForm.getValue('encryption_type') == 'aws:kms'">
+              <div class="form-group row">
+                <label class="cd-col-form-label required"
+                       for="kms_provider"
+                       i18n>KMS Provider</label>
+                <div class="cd-col-form-input">
+                  <select id="kms_provider"
+                          name="kms_provider"
+                          class="form-control"
+                          formControlName="kms_provider"
+                          [autofocus]="editing">
+                    <option i18n
+                            *ngIf="kmsProviders !== null"
+                            [ngValue]="null">-- Select a provider --</option>
+                    <option *ngFor="let provider of kmsProviders"
+                            [value]="provider">{{ provider }}</option>
+                  </select>
+                  <span class="invalid-feedback"
+                        *ngIf="bucketForm.showError('kms_provider', frm, 'required')"
+                        i18n>This field is required.</span>
+                </div>
+              </div>
+            </div>
+
+            <div *ngIf="bucketForm.getValue('encryption_type') == 'aws:kms'">
+              <div class="form-group row">
+                <label class="cd-col-form-label required"
+                       for="keyId"
+                       i18n>Key Id
+                </label>
+                <div class="cd-col-form-input">
+                  <input id="keyId"
+                         name="keyId"
+                         class="form-control"
+                         type="text"
+                         formControlName="keyId">
+                  <span class="invalid-feedback"
+                        *ngIf="bucketForm.showError('keyId', frm, 'required')"
+                        i18n>This field is required.</span>
+                </div>
+              </div>
+            </div>
+          </div>
+        </fieldset>
+
       </div>
       <div class="card-footer">
         <cd-form-button-panel (submitActionEvent)="submit()"
index 1d5aede396eae52b8fd6ec2d22738b670550ccea..aecb76925ccf5eedb470d28d5530c133e8491d1d 100644 (file)
@@ -1,5 +1,5 @@
-import { Component, OnInit } from '@angular/core';
-import { Validators } from '@angular/forms';
+import { AfterViewChecked, ChangeDetectorRef, Component, OnInit } from '@angular/core';
+import { AbstractControl, Validators } from '@angular/forms';
 import { ActivatedRoute, Router } from '@angular/router';
 
 import _ from 'lodash';
@@ -15,19 +15,24 @@ import { CdForm } from '~/app/shared/forms/cd-form';
 import { CdFormBuilder } from '~/app/shared/forms/cd-form-builder';
 import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
 import { CdValidators } from '~/app/shared/forms/cd-validators';
+import { ModalService } from '~/app/shared/services/modal.service';
 import { NotificationService } from '~/app/shared/services/notification.service';
+import { RgwBucketEncryptionModel } from '../models/rgw-bucket-encryption';
 import { RgwBucketMfaDelete } from '../models/rgw-bucket-mfa-delete';
 import { RgwBucketVersioning } from '../models/rgw-bucket-versioning';
+import { RgwConfigModalComponent } from '../rgw-config-modal/rgw-config-modal.component';
 
 @Component({
   selector: 'cd-rgw-bucket-form',
   templateUrl: './rgw-bucket-form.component.html',
-  styleUrls: ['./rgw-bucket-form.component.scss']
+  styleUrls: ['./rgw-bucket-form.component.scss'],
+  providers: [RgwBucketEncryptionModel]
 })
-export class RgwBucketFormComponent extends CdForm implements OnInit {
+export class RgwBucketFormComponent extends CdForm implements OnInit, AfterViewChecked {
   bucketForm: CdFormGroup;
   editing = false;
   owners: string[] = null;
+  kmsProviders: string[] = null;
   action: string;
   resource: string;
   zonegroup: string;
@@ -35,6 +40,8 @@ export class RgwBucketFormComponent extends CdForm implements OnInit {
   isVersioningAlreadyEnabled = false;
   isMfaDeleteAlreadyEnabled = false;
   icons = Icons;
+  kmsVaultConfig = false;
+  s3VaultConfig = false;
 
   get isVersioningEnabled(): boolean {
     return this.bucketForm.getValue('versioning');
@@ -49,9 +56,12 @@ export class RgwBucketFormComponent extends CdForm implements OnInit {
     private formBuilder: CdFormBuilder,
     private rgwBucketService: RgwBucketService,
     private rgwSiteService: RgwSiteService,
+    private modalService: ModalService,
     private rgwUserService: RgwUserService,
     private notificationService: NotificationService,
-    public actionLabels: ActionLabelsI18n
+    private rgwEncryptionModal: RgwBucketEncryptionModel,
+    public actionLabels: ActionLabelsI18n,
+    private readonly changeDetectorRef: ChangeDetectorRef
   ) {
     super();
     this.editing = this.router.url.startsWith(`/rgw/bucket/${URLVerbs.EDIT}`);
@@ -60,6 +70,10 @@ export class RgwBucketFormComponent extends CdForm implements OnInit {
     this.createForm();
   }
 
+  ngAfterViewChecked(): void {
+    this.changeDetectorRef.detectChanges();
+  }
+
   createForm() {
     const self = this;
     const lockDaysValidator = CdValidators.custom('lockDays', () => {
@@ -79,12 +93,31 @@ export class RgwBucketFormComponent extends CdForm implements OnInit {
           : [CdValidators.bucketName(), CdValidators.bucketExistence(false, this.rgwBucketService)]
       ],
       owner: [null, [Validators.required]],
+      kms_provider: ['vault'],
       'placement-target': [null, this.editing ? [] : [Validators.required]],
       versioning: [null],
       'mfa-delete': [null],
       'mfa-token-serial': [''],
       'mfa-token-pin': [''],
       lock_enabled: [{ value: false, disabled: this.editing }],
+      encryption_enabled: [null],
+      encryption_type: [
+        null,
+        [
+          CdValidators.requiredIf({
+            encryption_enabled: true
+          })
+        ]
+      ],
+      keyId: [
+        null,
+        [
+          CdValidators.requiredIf({
+            encryption_type: 'aws:kms',
+            encryption_enabled: true
+          })
+        ]
+      ],
       lock_mode: ['COMPLIANCE'],
       lock_retention_period_days: [0, [CdValidators.number(false), lockDaysValidator]]
     });
@@ -95,6 +128,21 @@ export class RgwBucketFormComponent extends CdForm implements OnInit {
       owners: this.rgwUserService.enumerate()
     };
 
+    this.kmsProviders = this.rgwEncryptionModal.kmsProviders;
+    this.rgwBucketService.getEncryptionConfig().subscribe((data) => {
+      this.kmsVaultConfig = data[0];
+      this.s3VaultConfig = data[1];
+      if (this.kmsVaultConfig && this.s3VaultConfig) {
+        this.bucketForm.get('encryption_type').setValue('');
+      } else if (this.kmsVaultConfig) {
+        this.bucketForm.get('encryption_type').setValue('aws:kms');
+      } else if (this.s3VaultConfig) {
+        this.bucketForm.get('encryption_type').setValue('AES256');
+      } else {
+        this.bucketForm.get('encryption_type').setValue('');
+      }
+    });
+
     if (!this.editing) {
       promises['getPlacementTargets'] = this.rgwSiteService.get('placement-targets');
     }
@@ -137,14 +185,14 @@ export class RgwBucketFormComponent extends CdForm implements OnInit {
           // the Angular react framework will throw an error if there is no
           // field for a given key.
           let value: object = _.pick(bidResp, _.keys(defaults));
+
           value['lock_retention_period_days'] = this.rgwBucketService.getLockDays(bidResp);
           value['placement-target'] = bidResp['placement_rule'];
           value['versioning'] = bidResp['versioning'] === RgwBucketVersioning.ENABLED;
           value['mfa-delete'] = bidResp['mfa_delete'] === RgwBucketMfaDelete.ENABLED;
-
+          value['encryption_enabled'] = bidResp['encryption'] === 'Enabled';
           // Append default values.
           value = _.merge(defaults, value);
-
           // Update the form.
           this.bucketForm.setValue(value);
           if (this.editing) {
@@ -156,7 +204,6 @@ export class RgwBucketFormComponent extends CdForm implements OnInit {
             }
           }
         }
-
         this.loadingReady();
       });
     });
@@ -168,6 +215,10 @@ export class RgwBucketFormComponent extends CdForm implements OnInit {
 
   submit() {
     // Exit immediately if the form isn't dirty.
+    if (this.bucketForm.getValue('encryption_enabled') == null) {
+      this.bucketForm.get('encryption_enabled').setValue(false);
+      this.bucketForm.get('encryption_type').setValue(null);
+    }
     if (this.bucketForm.pristine) {
       this.goToListView();
       return;
@@ -183,6 +234,9 @@ export class RgwBucketFormComponent extends CdForm implements OnInit {
           values['id'],
           values['owner'],
           versioning,
+          values['encryption_enabled'],
+          values['encryption_type'],
+          values['keyId'],
           mfaDelete,
           values['mfa-token-serial'],
           values['mfa-token-pin'],
@@ -212,7 +266,10 @@ export class RgwBucketFormComponent extends CdForm implements OnInit {
           values['placement-target'],
           values['lock_enabled'],
           values['lock_mode'],
-          values['lock_retention_period_days']
+          values['lock_retention_period_days'],
+          values['encryption_enabled'],
+          values['encryption_type'],
+          values['keyId']
         )
         .subscribe(
           () => {
@@ -261,4 +318,23 @@ export class RgwBucketFormComponent extends CdForm implements OnInit {
   getMfaDeleteStatus() {
     return this.isMfaDeleteEnabled ? RgwBucketMfaDelete.ENABLED : RgwBucketMfaDelete.DISABLED;
   }
+
+  fileUpload(files: FileList, controlName: string) {
+    const file: File = files[0];
+    const reader = new FileReader();
+    reader.addEventListener('load', () => {
+      const control: AbstractControl = this.bucketForm.get(controlName);
+      control.setValue(file);
+      control.markAsDirty();
+      control.markAsTouched();
+      control.updateValueAndValidity();
+    });
+  }
+
+  openConfigModal() {
+    const modalRef = this.modalService.show(RgwConfigModalComponent);
+    modalRef.componentInstance.configForm
+      .get('encryptionType')
+      .setValue(this.bucketForm.getValue('encryption_type'));
+  }
 }
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-config-modal/rgw-config-modal.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-config-modal/rgw-config-modal.component.html
new file mode 100644 (file)
index 0000000..88c3942
--- /dev/null
@@ -0,0 +1,251 @@
+<cd-modal [modalRef]="activeModal">
+  <ng-container i18n="form title"
+                class="modal-title">Update RGW Encryption Configurations</ng-container>
+
+  <ng-container class="modal-content">
+    <form name="configForm"
+          #frm="ngForm"
+          [formGroup]="configForm">
+    <div class="modal-body">
+      <label class="cd-col-form-label">Encryption Type:</label>
+      <div class="custom-control custom-radio custom-control-inline">
+        <input class="custom-control-input"
+               formControlName="encryptionType"
+               id="s3Enabled"
+               type="radio"
+               name="encryptionType"
+               value="AES256">
+        <label class="custom-control-label"
+               for="s3Enabled"
+               i18n>SSE-S3 Encryption</label>
+      </div>
+
+      <div class="custom-control custom-radio custom-control-inline">
+        <input class="custom-control-input"
+               formControlName="encryptionType"
+               id="kmsEnabled"
+               name="encryptionType"
+               value="aws:kms"
+               type="radio">
+        <label class="custom-control-label"
+               for="kmsEnabled"
+               i18n>SSE-KMS Encryption</label>
+      </div>
+
+      <div *ngIf="configForm.getValue('encryptionType') == 'aws:kms' || configForm.getValue('encryptionType') == 'AES256'">
+        <div class="form-group row">
+          <label class="cd-col-form-label required"
+                 for="kms_provider"
+                 i18n>Key management service provider</label>
+          <div class="cd-col-form-input">
+            <select id="kms_provider"
+                    name="kms_provider"
+                    class="form-control"
+                    formControlName="kms_provider">
+              <option i18n
+                      *ngIf="kmsProviders !== null"
+                      [ngValue]="null">-- Select a provider --</option>
+              <option *ngFor="let provider of kmsProviders"
+                      [value]="provider">{{ provider }}</option>
+            </select>
+            <span class="invalid-feedback"
+                  *ngIf="configForm.showError('kms_provider', frm, 'required')"
+                  i18n>This field is required.</span>
+          </div>
+        </div>
+      </div>
+
+      <div *ngIf="configForm.getValue('encryptionType') == 'aws:kms' || configForm.getValue('encryptionType') == 'AES256'">
+        <div class="form-group row">
+          <label class="cd-col-form-label required"
+                 for="auth_method"
+                 i18n>Authentication Method</label>
+          <div class="cd-col-form-input">
+            <select id="auth_method"
+                    name="auth_method"
+                    class="form-control"
+                    formControlName="auth_method">
+              <option i18n
+                      *ngIf="authMethods !== null"
+                      [ngValue]="null">-- Select a method --</option>
+              <option *ngFor="let auth_method of authMethods"
+                      [value]="auth_method">{{ auth_method }}</option>
+            </select>
+            <span class="invalid-feedback"
+                  *ngIf="configForm.showError('auth_method', frm, 'required')"
+                  i18n>This field is required.</span>
+          </div>
+        </div>
+      </div>
+
+      <div *ngIf="configForm.getValue('encryptionType') == 'aws:kms' || configForm.getValue('encryptionType') == 'AES256'">
+        <div class="form-group row">
+          <label class="cd-col-form-label required"
+                 for="secret_engine"
+                 i18n>Secret Engine</label>
+          <div class="cd-col-form-input">
+            <select id="secret_engine"
+                    name="secret_engine"
+                    class="form-control"
+                    formControlName="secret_engine">
+              <option i18n
+                      *ngIf="secretEngines !== null"
+                      [ngValue]="null">-- Select a method --</option>
+              <option *ngFor="let secret_engine of secretEngines"
+                      [value]="secret_engine">{{ secret_engine }}</option>
+            </select>
+            <span class="invalid-feedback"
+                  *ngIf="configForm.showError('secret_engine', frm, 'required')"
+                  i18n>This field is required.</span>
+          </div>
+        </div>
+      </div>
+
+      <div *ngIf="configForm.getValue('encryptionType') == 'aws:kms' || configForm.getValue('encryptionType') == 'AES256'">
+        <div class="form-group row">
+          <label class="cd-col-form-label"
+                 for="secret_path"
+                 i18n>Secret Path
+          </label>
+          <div class="cd-col-form-input">
+            <input id="secret_path"
+                   name="secret_path"
+                   class="form-control"
+                   type="text"
+                   formControlName="secret_path">
+            <span class="invalid-feedback"
+                  *ngIf="configForm.showError('secret_path', frm, 'required')"
+                  i18n>This field is required.</span>
+          </div>
+        </div>
+      </div>
+
+      <div *ngIf="configForm.getValue('encryptionType') == 'aws:kms' || configForm.getValue('encryptionType') == 'AES256'">
+        <div class="form-group row">
+          <label class="cd-col-form-label"
+                 for="namespace"
+                 i18n>Namespace
+          </label>
+          <div class="cd-col-form-input">
+            <input id="namespace"
+                   name="namespace"
+                   class="form-control"
+                   type="text"
+                   formControlName="namespace">
+          </div>
+        </div>
+      </div>
+
+      <div *ngIf="configForm.getValue('encryptionType') == 'aws:kms' || configForm.getValue('encryptionType') == 'AES256'">
+        <div class="form-group row">
+          <label class="cd-col-form-label required"
+                 for="address"
+                 i18n>Vault Address
+          </label>
+          <div class="cd-col-form-input">
+            <input id="address"
+                   name="address"
+                   class="form-control"
+                   formControlName="address">
+            <span class="invalid-feedback"
+                  *ngIf="configForm.showError('address', frm, 'required')"
+                  i18n>This field is required.</span>
+          </div>
+        </div>
+      </div>
+
+      <div *ngIf="configForm.getValue('auth_method') == 'token'"
+           class="form-group row">
+        <label class="cd-col-form-label required"
+               for="token">
+        <span i18n>Token</span>
+        </label>
+        <div class="cd-col-form-input">
+          <input type="file"
+                 formControlName="token"
+                 (change)="fileUpload($event.target.files, 'token')">
+          <span class="invalid-feedback"
+                *ngIf="configForm.showError('token', frm, 'required')"
+                i18n>This field is required.</span>
+        </div>
+      </div>
+
+      <div *ngIf="configForm.getValue('auth_method') == 'agent'">
+        <div class="form-group row">
+          <label class="cd-col-form-label required"
+                 for="role"
+                 i18n>Role
+          </label>
+          <div class="cd-col-form-input">
+            <input id="role"
+                   name="role"
+                   class="form-control"
+                   formControlName="role">
+            <span class="invalid-feedback"
+                  *ngIf="configForm.showError('role', frm, 'required')"
+                  i18n>This field is required.</span>
+          </div>
+        </div>
+      </div>
+
+      <div *ngIf="configForm.getValue('encryptionType') == 'aws:kms' || configForm.getValue('encryptionType') == 'AES256'">
+        <div class="form-group row">
+          <label class="cd-col-form-label"
+                 for="ssl_cert">
+          <span i18n>CA Certificate</span>
+          <cd-helper i18n>The SSL certificate in PEM format.</cd-helper>
+          </label>
+          <div class="cd-col-form-input">
+            <input type="file"
+                   formControlName="ssl_cert"
+                   (change)="fileUpload($event.target.files, 'ssl_cert')">
+            <span class="invalid-feedback"
+                  *ngIf="configForm.showError('ssl_cert', frm, 'required')"
+                  i18n>This field is required.</span>
+          </div>
+        </div>
+      </div>
+
+      <div *ngIf="configForm.getValue('encryptionType') == 'aws:kms' || configForm.getValue('encryptionType') == 'AES256'">
+        <div class="form-group row">
+          <label class="cd-col-form-label"
+                 for="client_cert">
+          <span i18n>Client Certificate</span>
+          <cd-helper i18n>The Client certificate in PEM format.</cd-helper>
+          </label>
+          <div class="cd-col-form-input">
+            <input type="file"
+                   formControlName="client_cert"
+                   (change)="fileUpload($event.target.files, 'client_cert')">
+            <span class="invalid-feedback"
+                  *ngIf="configForm.showError('client_cert', frm, 'required')"
+                  i18n>This field is required.</span>
+          </div>
+        </div>
+      </div>
+
+      <div *ngIf="configForm.getValue('encryptionType') == 'aws:kms' || configForm.getValue('encryptionType') == 'AES256'">
+        <div class="form-group row">
+          <label class="cd-col-form-label"
+                 for="client_key">
+          <span i18n>Client Private Key</span>
+          <cd-helper i18n>The Client Private Key in PEM format.</cd-helper>
+          </label>
+          <div class="cd-col-form-input">
+            <input type="file"
+                   (change)="fileUpload($event.target.files, 'client_key')">
+            <span class="invalid-feedback"
+                  *ngIf="configForm.showError('client_key', frm, 'required')"
+                  i18n>This field is required.</span>
+          </div>
+        </div>
+      </div>
+    </div>
+    <div class="modal-footer">
+      <cd-form-button-panel (submitActionEvent)="onSubmit()"
+                            [submitText]="actionLabels.SUBMIT"
+                            [form]="configForm"></cd-form-button-panel>
+    </div>
+    </form>
+  </ng-container>
+</cd-modal>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-config-modal/rgw-config-modal.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-config-modal/rgw-config-modal.component.scss
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-config-modal/rgw-config-modal.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-config-modal/rgw-config-modal.component.spec.ts
new file mode 100644 (file)
index 0000000..a266615
--- /dev/null
@@ -0,0 +1,38 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ReactiveFormsModule } from '@angular/forms';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import { ToastrModule } from 'ngx-toastr';
+
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { RgwConfigModalComponent } from './rgw-config-modal.component';
+
+describe('RgwConfigModalComponent', () => {
+  let component: RgwConfigModalComponent;
+  let fixture: ComponentFixture<RgwConfigModalComponent>;
+
+  configureTestBed({
+    declarations: [RgwConfigModalComponent],
+    imports: [
+      SharedModule,
+      ReactiveFormsModule,
+      RouterTestingModule,
+      HttpClientTestingModule,
+      ToastrModule.forRoot()
+    ],
+    providers: [NgbActiveModal]
+  });
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(RgwConfigModalComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-config-modal/rgw-config-modal.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-config-modal/rgw-config-modal.component.ts
new file mode 100644 (file)
index 0000000..2b45055
--- /dev/null
@@ -0,0 +1,144 @@
+import { Component, EventEmitter, OnInit, Output } from '@angular/core';
+import { AbstractControl, Validators } from '@angular/forms';
+import { Router } from '@angular/router';
+
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import _ from 'lodash';
+
+import { RgwBucketService } from '~/app/shared/api/rgw-bucket.service';
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { NotificationType } from '~/app/shared/enum/notification-type.enum';
+import { CdFormBuilder } from '~/app/shared/forms/cd-form-builder';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { CdValidators } from '~/app/shared/forms/cd-validators';
+import { NotificationService } from '~/app/shared/services/notification.service';
+import { RgwBucketEncryptionModel } from '../models/rgw-bucket-encryption';
+
+@Component({
+  selector: 'cd-rgw-config-modal',
+  templateUrl: './rgw-config-modal.component.html',
+  styleUrls: ['./rgw-config-modal.component.scss'],
+  providers: [RgwBucketEncryptionModel]
+})
+export class RgwConfigModalComponent implements OnInit {
+  readonly vaultAddress = /^((https?:\/\/)|(www.))(?:([a-zA-Z]+)|(\d+\.\d+.\d+.\d+)):\d{4}$/;
+
+  kmsProviders: string[];
+
+  configForm: CdFormGroup;
+
+  @Output()
+  submitAction = new EventEmitter();
+  authMethods: string[];
+  secretEngines: string[];
+
+  constructor(
+    private formBuilder: CdFormBuilder,
+    public activeModal: NgbActiveModal,
+    private router: Router,
+    public actionLabels: ActionLabelsI18n,
+    private rgwBucketService: RgwBucketService,
+    private rgwEncryptionModal: RgwBucketEncryptionModel,
+    private notificationService: NotificationService
+  ) {
+    this.createForm();
+  }
+  ngOnInit(): void {
+    this.kmsProviders = this.rgwEncryptionModal.kmsProviders;
+    this.authMethods = this.rgwEncryptionModal.authMethods;
+    this.secretEngines = this.rgwEncryptionModal.secretEngines;
+  }
+
+  createForm() {
+    this.configForm = this.formBuilder.group({
+      address: [
+        null,
+        [
+          Validators.required,
+          CdValidators.custom('vaultPattern', (value: string) => {
+            if (_.isEmpty(value)) {
+              return false;
+            }
+            return !this.vaultAddress.test(value);
+          })
+        ]
+      ],
+      kms_provider: ['vault', Validators.required],
+      encryptionType: [null, Validators.required],
+      auth_method: [null, Validators.required],
+      secret_engine: [null, Validators.required],
+      secret_path: ['/'],
+      namespace: [null],
+      token: [
+        null,
+        [
+          CdValidators.requiredIf({
+            auth_method: 'token'
+          })
+        ]
+      ],
+      ssl_cert: [null, CdValidators.sslCert()],
+      client_cert: [null, CdValidators.pemCert()],
+      client_key: [null, CdValidators.sslPrivKey()],
+      role: [
+        null,
+        [
+          CdValidators.requiredIf({
+            auth_method: 'agent'
+          })
+        ]
+      ],
+      kmsEnabled: [{ value: false }],
+      s3Enabled: [{ value: false }]
+    });
+  }
+
+  fileUpload(files: FileList, controlName: string) {
+    const file: File = files[0];
+    const reader = new FileReader();
+    reader.addEventListener('load', () => {
+      const control: AbstractControl = this.configForm.get(controlName);
+      control.setValue(file);
+      control.markAsDirty();
+      control.markAsTouched();
+      control.updateValueAndValidity();
+    });
+  }
+
+  onSubmit() {
+    const values = this.configForm.value;
+    this.rgwBucketService
+      .setEncryptionConfig(
+        values['encryptionType'],
+        values['kms_provider'],
+        values['auth_method'],
+        values['secret_engine'],
+        values['secret_path'],
+        values['namespace'],
+        values['address'],
+        values['token'],
+        values['owner'],
+        values['ssl_cert'],
+        values['client_cert'],
+        values['client_key']
+      )
+      .subscribe({
+        next: () => {
+          this.notificationService.show(
+            NotificationType.success,
+            $localize`Updated RGW Encryption Configuration values`
+          );
+        },
+        error: (error: any) => {
+          this.notificationService.show(NotificationType.error, error);
+          this.configForm.setErrors({ cdSubmitButton: true });
+        },
+        complete: () => {
+          this.activeModal.close();
+          this.router.routeReuseStrategy.shouldReuseRoute = () => false;
+          this.router.onSameUrlNavigation = 'reload';
+          this.router.navigate([this.router.url]);
+        }
+      });
+  }
+}
index 4abcd69796f32d51b3babcff07b7fa90414067a8..7ecaddddfef08f3946443fcbadea8ddaf9e4604f 100644 (file)
@@ -12,6 +12,7 @@ import { PerformanceCounterModule } from '../performance-counter/performance-cou
 import { RgwBucketDetailsComponent } from './rgw-bucket-details/rgw-bucket-details.component';
 import { RgwBucketFormComponent } from './rgw-bucket-form/rgw-bucket-form.component';
 import { RgwBucketListComponent } from './rgw-bucket-list/rgw-bucket-list.component';
+import { RgwConfigModalComponent } from './rgw-config-modal/rgw-config-modal.component';
 import { RgwDaemonDetailsComponent } from './rgw-daemon-details/rgw-daemon-details.component';
 import { RgwDaemonListComponent } from './rgw-daemon-list/rgw-daemon-list.component';
 import { RgwUserCapabilityModalComponent } from './rgw-user-capability-modal/rgw-user-capability-modal.component';
@@ -56,7 +57,8 @@ import { RgwUserSwiftKeyModalComponent } from './rgw-user-swift-key-modal/rgw-us
     RgwUserSwiftKeyModalComponent,
     RgwUserS3KeyModalComponent,
     RgwUserCapabilityModalComponent,
-    RgwUserSubuserModalComponent
+    RgwUserSubuserModalComponent,
+    RgwConfigModalComponent
   ]
 })
 export class RgwModule {}
index b22b67e349123c244dc7ad0f24c2f7c8c9fe4001..2c42d8b427c0d0f531ecfb430e75376f88e0a551 100644 (file)
@@ -49,20 +49,44 @@ describe('RgwBucketService', () => {
 
   it('should call create', () => {
     service
-      .create('foo', 'bar', 'default', 'default-placement', false, 'COMPLIANCE', '5')
+      .create(
+        'foo',
+        'bar',
+        'default',
+        'default-placement',
+        false,
+        'COMPLIANCE',
+        '5',
+        true,
+        'aws:kms',
+        'qwerty1'
+      )
       .subscribe();
     const req = httpTesting.expectOne(
-      `api/rgw/bucket?bucket=foo&uid=bar&zonegroup=default&placement_target=default-placement&lock_enabled=false&lock_mode=COMPLIANCE&lock_retention_period_days=5&${RgwHelper.DAEMON_QUERY_PARAM}`
+      `api/rgw/bucket?bucket=foo&uid=bar&zonegroup=default&placement_target=default-placement&lock_enabled=false&lock_mode=COMPLIANCE&lock_retention_period_days=5&encryption_state=true&encryption_type=aws%253Akms&key_id=qwerty1&${RgwHelper.DAEMON_QUERY_PARAM}`
     );
     expect(req.request.method).toBe('POST');
   });
 
   it('should call update', () => {
     service
-      .update('foo', 'bar', 'baz', 'Enabled', 'Enabled', '1', '223344', 'GOVERNANCE', '10')
+      .update(
+        'foo',
+        'bar',
+        'baz',
+        'Enabled',
+        true,
+        'aws:kms',
+        'qwerty1',
+        'Enabled',
+        '1',
+        '223344',
+        'GOVERNANCE',
+        '10'
+      )
       .subscribe();
     const req = httpTesting.expectOne(
-      `api/rgw/bucket/foo?${RgwHelper.DAEMON_QUERY_PARAM}&bucket_id=bar&uid=baz&versioning_state=Enabled&mfa_delete=Enabled&mfa_token_serial=1&mfa_token_pin=223344&lock_mode=GOVERNANCE&lock_retention_period_days=10`
+      `api/rgw/bucket/foo?${RgwHelper.DAEMON_QUERY_PARAM}&bucket_id=bar&uid=baz&versioning_state=Enabled&encryption_state=true&encryption_type=aws%253Akms&key_id=qwerty1&mfa_delete=Enabled&mfa_token_serial=1&mfa_token_pin=223344&lock_mode=GOVERNANCE&lock_retention_period_days=10`
     );
     expect(req.request.method).toBe('PUT');
   });
index fc88bfa7181649658c4c04005d7dd12ef750f7a7..3be9576fa657a0c67655c7882617bd1784eb5e06 100644 (file)
@@ -50,7 +50,10 @@ export class RgwBucketService extends ApiClient {
     placementTarget: string,
     lockEnabled: boolean,
     lock_mode: 'GOVERNANCE' | 'COMPLIANCE',
-    lock_retention_period_days: string
+    lock_retention_period_days: string,
+    encryption_state: boolean,
+    encryption_type: string,
+    key_id: string
   ) {
     return this.rgwDaemonService.request((params: HttpParams) => {
       return this.http.post(this.url, null, {
@@ -63,6 +66,9 @@ export class RgwBucketService extends ApiClient {
             lock_enabled: String(lockEnabled),
             lock_mode,
             lock_retention_period_days,
+            encryption_state: String(encryption_state),
+            encryption_type,
+            key_id,
             daemon_name: params.get('daemon_name')
           }
         })
@@ -75,6 +81,9 @@ export class RgwBucketService extends ApiClient {
     bucketId: string,
     uid: string,
     versioningState: string,
+    encryptionState: boolean,
+    encryptionType: string,
+    keyId: string,
     mfaDelete: string,
     mfaTokenSerial: string,
     mfaTokenPin: string,
@@ -82,14 +91,19 @@ export class RgwBucketService extends ApiClient {
     lockRetentionPeriodDays: string
   ) {
     return this.rgwDaemonService.request((params: HttpParams) => {
-      params = params.append('bucket_id', bucketId);
-      params = params.append('uid', uid);
-      params = params.append('versioning_state', versioningState);
-      params = params.append('mfa_delete', mfaDelete);
-      params = params.append('mfa_token_serial', mfaTokenSerial);
-      params = params.append('mfa_token_pin', mfaTokenPin);
-      params = params.append('lock_mode', lockMode);
-      params = params.append('lock_retention_period_days', lockRetentionPeriodDays);
+      params = params.appendAll({
+        bucket_id: bucketId,
+        uid: uid,
+        versioning_state: versioningState,
+        encryption_state: String(encryptionState),
+        encryption_type: encryptionType,
+        key_id: keyId,
+        mfa_delete: mfaDelete,
+        mfa_token_serial: mfaTokenSerial,
+        mfa_token_pin: mfaTokenPin,
+        lock_mode: lockMode,
+        lock_retention_period_days: lockRetentionPeriodDays
+      });
       return this.http.put(`${this.url}/${bucket}`, null, { params: params });
     });
   }
@@ -125,4 +139,55 @@ export class RgwBucketService extends ApiClient {
 
     return bucketData['lock_retention_period_days'] || 0;
   }
+
+  setEncryptionConfig(
+    encryption_type: string,
+    kms_provider: string,
+    auth_method: string,
+    secret_engine: string,
+    secret_path: string,
+    namespace: string,
+    address: string,
+    token: string,
+    owner: string,
+    ssl_cert: string,
+    client_cert: string,
+    client_key: string
+  ) {
+    return this.rgwDaemonService.request((params: HttpParams) => {
+      params = params.appendAll({
+        encryption_type: encryption_type,
+        kms_provider: kms_provider,
+        auth_method: auth_method,
+        secret_engine: secret_engine,
+        secret_path: secret_path,
+        namespace: namespace,
+        address: address,
+        token: token,
+        owner: owner,
+        ssl_cert: ssl_cert,
+        client_cert: client_cert,
+        client_key: client_key
+      });
+      return this.http.put(`${this.url}/setEncryptionConfig`, null, { params: params });
+    });
+  }
+
+  getEncryption(bucket: string) {
+    return this.rgwDaemonService.request((params: HttpParams) => {
+      return this.http.get(`${this.url}/${bucket}/getEncryption`, { params: params });
+    });
+  }
+
+  deleteEncryption(bucket: string) {
+    return this.rgwDaemonService.request((params: HttpParams) => {
+      return this.http.get(`${this.url}/${bucket}/deleteEncryption`, { params: params });
+    });
+  }
+
+  getEncryptionConfig() {
+    return this.rgwDaemonService.request(() => {
+      return this.http.get(`${this.url}/getEncryptionConfig`);
+    });
+  }
 }
index 8655a57364612e46ccdfc7b5b2f1ef1466a96359..b445db65cf57cea0f950d7a126a4092907c5dc26 100644 (file)
@@ -7607,6 +7607,13 @@ paths:
                   type: string
                 daemon_name:
                   type: string
+                encryption_state:
+                  default: 'false'
+                  type: string
+                encryption_type:
+                  type: string
+                key_id:
+                  type: string
                 lock_enabled:
                   default: 'false'
                   type: string
@@ -7650,6 +7657,168 @@ paths:
       - jwt: []
       tags:
       - RgwBucket
+  /api/rgw/bucket/deleteEncryption:
+    delete:
+      parameters:
+      - in: query
+        name: bucket_name
+        required: true
+        schema:
+          type: string
+      - allowEmptyValue: true
+        in: query
+        name: daemon_name
+        schema:
+          type: string
+      - allowEmptyValue: true
+        in: query
+        name: owner
+        schema:
+          type: string
+      responses:
+        '202':
+          content:
+            application/vnd.ceph.api.v1.0+json:
+              type: object
+          description: Operation is still executing. Please check the task queue.
+        '204':
+          content:
+            application/vnd.ceph.api.v1.0+json:
+              type: object
+          description: Resource deleted.
+        '400':
+          description: Operation exception. Please check the response body for details.
+        '401':
+          description: Unauthenticated access. Please login first.
+        '403':
+          description: Unauthorized access. Please check your permissions.
+        '500':
+          description: Unexpected error. Please check the response body for the stack
+            trace.
+      security:
+      - jwt: []
+      tags:
+      - RgwBucket
+  /api/rgw/bucket/getEncryption:
+    get:
+      parameters:
+      - in: query
+        name: bucket_name
+        required: true
+        schema:
+          type: string
+      - allowEmptyValue: true
+        in: query
+        name: daemon_name
+        schema:
+          type: string
+      - allowEmptyValue: true
+        in: query
+        name: owner
+        schema:
+          type: string
+      responses:
+        '200':
+          content:
+            application/vnd.ceph.api.v1.0+json:
+              type: object
+          description: OK
+        '400':
+          description: Operation exception. Please check the response body for details.
+        '401':
+          description: Unauthenticated access. Please login first.
+        '403':
+          description: Unauthorized access. Please check your permissions.
+        '500':
+          description: Unexpected error. Please check the response body for the stack
+            trace.
+      security:
+      - jwt: []
+      tags:
+      - RgwBucket
+  /api/rgw/bucket/getEncryptionConfig:
+    get:
+      parameters: []
+      responses:
+        '200':
+          content:
+            application/vnd.ceph.api.v1.0+json:
+              type: object
+          description: OK
+        '400':
+          description: Operation exception. Please check the response body for details.
+        '401':
+          description: Unauthenticated access. Please login first.
+        '403':
+          description: Unauthorized access. Please check your permissions.
+        '500':
+          description: Unexpected error. Please check the response body for the stack
+            trace.
+      security:
+      - jwt: []
+      tags:
+      - RgwBucket
+  /api/rgw/bucket/setEncryptionConfig:
+    put:
+      parameters: []
+      requestBody:
+        content:
+          application/json:
+            schema:
+              properties:
+                address:
+                  type: string
+                auth_method:
+                  type: string
+                client_cert:
+                  type: string
+                client_key:
+                  type: string
+                daemon_name:
+                  type: string
+                encryption_type:
+                  type: string
+                kms_provider:
+                  type: string
+                namespace:
+                  default: ''
+                  type: string
+                owner:
+                  type: string
+                secret_engine:
+                  type: string
+                secret_path:
+                  default: ''
+                  type: string
+                ssl_cert:
+                  type: string
+                token:
+                  type: string
+              type: object
+      responses:
+        '200':
+          content:
+            application/vnd.ceph.api.v1.0+json:
+              type: object
+          description: Resource updated.
+        '202':
+          content:
+            application/vnd.ceph.api.v1.0+json:
+              type: object
+          description: Operation is still executing. Please check the task queue.
+        '400':
+          description: Operation exception. Please check the response body for details.
+        '401':
+          description: Unauthenticated access. Please login first.
+        '403':
+          description: Unauthorized access. Please check your permissions.
+        '500':
+          description: Unexpected error. Please check the response body for the stack
+            trace.
+      security:
+      - jwt: []
+      tags:
+      - RgwBucket
   /api/rgw/bucket/{bucket}:
     delete:
       parameters:
@@ -7739,6 +7908,13 @@ paths:
                   type: string
                 daemon_name:
                   type: string
+                encryption_state:
+                  default: 'false'
+                  type: string
+                encryption_type:
+                  type: string
+                key_id:
+                  type: string
                 lock_mode:
                   type: string
                 lock_retention_period_days:
index 518ee12a2f5450526f8cb31d6adf84bf33f48108..f69070a206b5d3930056f3adcbdeab4159f221a1 100644 (file)
@@ -5,7 +5,7 @@ import logging
 
 import rados
 from mgr_module import CommandResult
-from mgr_util import get_most_recent_rate, get_time_series_rates
+from mgr_util import get_most_recent_rate, get_time_series_rates, name_to_config_section
 
 from .. import mgr
 
@@ -182,6 +182,113 @@ class CephService(object):
                 return pool
         return None
 
+    @classmethod
+    def get_encryption_config(cls):
+        kms_vault_configured = False
+        s3_vault_configured = False
+        kms_backend: str = ''
+        sse_s3_backend: str = ''
+        vault_stats = []
+
+        kms_backend = CephService.send_command('mon', 'config get',
+                                               who=name_to_config_section('rgw'),
+                                               key='rgw_crypt_s3_kms_backend')
+        sse_s3_backend = CephService.send_command('mon', 'config get',
+                                                  who=name_to_config_section('rgw'),
+                                                  key='rgw_crypt_sse_s3_backend')
+
+        if kms_backend.strip() == 'vault':
+            kms_vault_auth: str = CephService.send_command('mon', 'config get',
+                                                           who=name_to_config_section('rgw'),
+                                                           key='rgw_crypt_vault_auth')
+            kms_vault_engine: str = CephService.send_command('mon', 'config get',
+                                                             who=name_to_config_section('rgw'),
+                                                             key='rgw_crypt_vault_secret_engine')
+            kms_vault_address: str = CephService.send_command('mon', 'config get',
+                                                              who=name_to_config_section('rgw'),
+                                                              key='rgw_crypt_vault_addr')
+            kms_vault_token: str = CephService.send_command('mon', 'config get',
+                                                            who=name_to_config_section('rgw'),
+                                                            key='rgw_crypt_vault_token_file')
+            if (
+                kms_vault_auth.strip() != ""
+                and kms_vault_engine.strip() != ""
+                and kms_vault_address.strip() != ""
+                and kms_vault_token.strip() != ""
+            ):
+                kms_vault_configured = True
+
+        if sse_s3_backend.strip() == 'vault':
+            s3_vault_auth: str = CephService.send_command('mon', 'config get',
+                                                          who=name_to_config_section('rgw'),
+                                                          key='rgw_crypt_sse_s3_vault_auth')
+            s3_vault_engine: str = CephService.send_command('mon',
+                                                            'config get',
+                                                            who=name_to_config_section('rgw'),
+                                                            key='rgw_crypt_sse_s3_vault_secret_engine')  # noqa E501 #pylint: disable=line-too-long
+            s3_vault_address: str = CephService.send_command('mon', 'config get',
+                                                             who=name_to_config_section('rgw'),
+                                                             key='rgw_crypt_sse_s3_vault_addr')
+            s3_vault_token: str = CephService.send_command('mon', 'config get',
+                                                           who=name_to_config_section('rgw'),
+                                                           key='rgw_crypt_sse_s3_vault_token_file')
+            if (
+                s3_vault_auth.strip() != ""
+                and s3_vault_engine.strip() != ""
+                and s3_vault_address.strip() != ""
+                and s3_vault_token.strip() != ""
+            ):
+                s3_vault_configured = True
+
+        vault_stats.append(kms_vault_configured)
+        vault_stats.append(s3_vault_configured)
+        return vault_stats
+
+    @classmethod
+    def set_encryption_config(cls, encryption_type, kms_provider, auth_method,
+                              secret_engine, secret_path, namespace, address,
+                              token, ssl_cert, client_cert, client_key):
+
+        if encryption_type == 'aws:kms':
+
+            KMS_CONFIG = [
+                ['rgw_crypt_s3_kms_backend', kms_provider],
+                ['rgw_crypt_vault_auth', auth_method],
+                ['rgw_crypt_vault_prefix', secret_path],
+                ['rgw_crypt_vault_namespace', namespace],
+                ['rgw_crypt_vault_secret_engine', secret_engine],
+                ['rgw_crypt_vault_addr', address],
+                ['rgw_crypt_vault_token_file', token],
+                ['rgw_crypt_vault_ssl_cacert', ssl_cert],
+                ['rgw_crypt_vault_ssl_clientcert', client_cert],
+                ['rgw_crypt_vault_ssl_clientkey', client_key]
+            ]
+
+            for (key, value) in KMS_CONFIG:
+                CephService.send_command('mon', 'config set', who=name_to_config_section('rgw'),
+                                         name=key, value=value)
+
+        if encryption_type == 'AES256':
+
+            SSE_S3_CONFIG = [
+                ['rgw_crypt_sse_s3_backend', kms_provider],
+                ['rgw_crypt_sse_s3_vault_auth', auth_method],
+                ['rgw_crypt_sse_s3_vault_prefix', secret_path],
+                ['rgw_crypt_sse_s3_vault_namespace', namespace],
+                ['rgw_crypt_sse_s3_vault_secret_engine', secret_engine],
+                ['rgw_crypt_sse_s3_vault_addr', address],
+                ['rgw_crypt_sse_s3_vault_token_file', token],
+                ['rgw_crypt_sse_s3_vault_ssl_cacert', ssl_cert],
+                ['rgw_crypt_sse_s3_vault_ssl_clientcert', client_cert],
+                ['rgw_crypt_sse_s3_vault_ssl_clientkey', client_key]
+            ]
+
+            for (key, value) in SSE_S3_CONFIG:
+                CephService.send_command('mon', 'config set', who=name_to_config_section('rgw'),
+                                         name=key, value=value)
+
+        return {}
+
     @classmethod
     def get_pool_pg_status(cls, pool_name):
         # type: (str) -> dict
index 33a6761ae6b6a3b39eb31c7bcbeef19f61fd7d8d..242600c2265b07098fb0202b137ec4b8e2882469 100644 (file)
@@ -267,6 +267,7 @@ def configure_rgw_credentials():
         raise NoCredentialsException
 
 
+# pylint: disable=R0904
 class RgwClient(RestClient):
     _host = None
     _port = None
@@ -646,6 +647,50 @@ class RgwClient(RestClient):
                                      http_status_code=error.status_code,
                                      component='rgw')
 
+    @RestClient.api_get('/{bucket_name}?encryption')
+    def get_bucket_encryption(self, bucket_name, request=None):
+        # pylint: disable=unused-argument
+        try:
+            result = request()  # type: ignore
+            result['Status'] = 'Enabled'
+            return result
+        except RequestException as e:
+            if e.content:
+                content = json_str_to_object(e.content)
+                if content.get(
+                        'Code') == 'ServerSideEncryptionConfigurationNotFoundError':
+                    return {
+                        'Status': 'Disabled',
+                    }
+            raise e
+
+    @RestClient.api_delete('/{bucket_name}?encryption')
+    def delete_bucket_encryption(self, bucket_name, request=None):
+        # pylint: disable=unused-argument
+        result = request()  # type: ignore
+        return result
+
+    @RestClient.api_put('/{bucket_name}?encryption')
+    def set_bucket_encryption(self, bucket_name, key_id,
+                              sse_algorithm, request: Optional[object] = None):
+        # pylint: disable=unused-argument
+        encryption_configuration = ET.Element('ServerSideEncryptionConfiguration')
+        rule_element = ET.SubElement(encryption_configuration, 'Rule')
+        default_encryption_element = ET.SubElement(rule_element,
+                                                   'ApplyServerSideEncryptionByDefault')
+        sse_algo_element = ET.SubElement(default_encryption_element,
+                                         'SSEAlgorithm')
+        sse_algo_element.text = sse_algorithm
+        if sse_algorithm == 'aws:kms':
+            kms_master_key_element = ET.SubElement(default_encryption_element,
+                                                   'KMSMasterKeyID')
+            kms_master_key_element.text = key_id
+        data = ET.tostring(encryption_configuration, encoding='unicode')
+        try:
+            _ = request(data=data)  # type: ignore
+        except RequestException as e:
+            raise DashboardException(msg=str(e), component='rgw')
+
     @RestClient.api_get('/{bucket_name}?object-lock')
     def get_bucket_locking(self, bucket_name, request=None):
         # type: (str, Optional[object]) -> dict
index 1a86967cb4a32c797827367fac96132dd855be75..ddc9bfa4a2dac7adb3b9de3387b22c4225ec69a7 100644 (file)
@@ -12,7 +12,7 @@ import time
 import logging
 import sys
 from threading import Lock, Condition, Event
-from typing import no_type_check
+from typing import no_type_check, NewType
 import urllib
 from functools import wraps
 if sys.version_info >= (3, 3):
@@ -29,6 +29,8 @@ T = TypeVar('T')
 if TYPE_CHECKING:
     from mgr_module import MgrModule
 
+ConfEntity = NewType('ConfEntity', str)
+
 Module_T = TypeVar('Module_T', bound="MgrModule")
 
 (
@@ -700,6 +702,18 @@ def get_time_series_rates(data: List[Tuple[float, float]]) -> List[Tuple[float,
     return [(data2[0], _derivative(data1, data2) if data1 is not None else 0.0) for data1, data2 in
             _pairwise(data)]
 
+def name_to_config_section(name: str) -> ConfEntity:
+    """
+    Map from daemon names to ceph entity names (as seen in config)
+    """
+    daemon_type = name.split('.', 1)[0]
+    if daemon_type in ['rgw', 'rbd-mirror', 'nfs', 'crash', 'iscsi']:
+        return ConfEntity('client.' + name)
+    elif daemon_type in ['mon', 'osd', 'mds', 'mgr', 'client']:
+        return ConfEntity(name)
+    else:
+        return ConfEntity('mon')
+
 
 def _filter_time_series(data: List[Tuple[float, float]]) -> List[Tuple[float, float]]:
     """ Filters time series data