From: Aashish Sharma Date: Tue, 18 Jun 2024 11:13:07 +0000 (+0530) Subject: mgr/dashboard: add a new configuration page in side nav bar Object > X-Git-Tag: v20.0.0~1579^2 X-Git-Url: http://git.apps.os.sepia.ceph.com/?a=commitdiff_plain;h=75a4fe9548ebad4357cdcd59e8a08d73ae00c28c;p=ceph.git mgr/dashboard: add a new configuration page in side nav bar Object > Configuration Fixes: https://tracker.ceph.com/issues/66543 Signed-off-by: Aashish Sharma --- diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/buckets.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/buckets.e2e-spec.ts index 8b05c309f695a..4bfc672ccf206 100644 --- a/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/buckets.e2e-spec.ts +++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/buckets.e2e-spec.ts @@ -31,11 +31,6 @@ describe('RGW buckets page', () => { buckets.delete(bucket_name); }); - it('should check default encryption is SSE-S3', () => { - buckets.navigateTo('create'); - buckets.checkForDefaultEncryption(); - }); - it('should create bucket with object locking enabled', () => { buckets.navigateTo('create'); buckets.create(bucket_name, BucketsPageHelper.USERS[0], true); diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/buckets.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/buckets.po.ts index 32f89c263a439..069b48f888d6a 100644 --- a/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/buckets.po.ts +++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/buckets.po.ts @@ -50,14 +50,6 @@ export class BucketsPageHelper extends PageHelper { this.getFirstTableCell(name).should('exist'); } - @PageHelper.restrictTo(pages.create.url) - checkForDefaultEncryption() { - cy.get("a[aria-label='click here']").click(); - cy.get('cd-modal').within(() => { - cy.get('input[id=s3Enabled]').should('be.checked'); - }); - } - @PageHelper.restrictTo(pages.index.url) edit(name: string, new_owner: string, isLocking = false) { this.navigateEdit(name); diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/configuration.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/configuration.e2e-spec.ts new file mode 100644 index 0000000000000..d1e4836aeb172 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/configuration.e2e-spec.ts @@ -0,0 +1,36 @@ +import { ConfigurationPageHelper } from './configuration.po'; + +describe('RGW configuration page', () => { + const configurations = new ConfigurationPageHelper(); + + beforeEach(() => { + cy.login(); + configurations.navigateTo(); + }); + + describe('breadcrumb and tab tests', () => { + it('should open and show breadcrumb', () => { + configurations.expectBreadcrumbText('Configuration'); + }); + + it('should show one tab', () => { + configurations.getTabsCount().should('eq', 1); + }); + + it('should show Server-side Encryption Config list tab at first', () => { + configurations.getTabText(0).should('eq', 'Server-side Encryption'); + }); + }); + + describe('create and edit encryption configuration', () => { + it('should create configuration', () => { + configurations.create('vault', 'agent', 'transit', 'https://localhost:8080'); + configurations.getFirstTableCell('SSE_KMS').should('exist'); + }); + + it('should edit configuration', () => { + configurations.edit('https://localhost:9090'); + configurations.getDataTables().should('contain.text', 'https://localhost:9090'); + }); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/configuration.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/configuration.po.ts new file mode 100644 index 0000000000000..a1f4a9fbaf052 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/configuration.po.ts @@ -0,0 +1,52 @@ +import { PageHelper } from '../page-helper.po'; + +export class ConfigurationPageHelper extends PageHelper { + pages = { + index: { url: '#/rgw/configuration', id: 'cd-rgw-configuration-page' } + }; + + columnIndex = { + address: 4 + }; + + create(provider: string, auth_method: string, secret_engine: string, address: string) { + cy.contains('button', 'Create').click(); + this.selectKmsProvider(provider); + cy.get('#kms_provider').should('have.class', 'ng-valid'); + this.selectAuthMethod(auth_method); + cy.get('#auth_method').should('have.class', 'ng-valid'); + this.selectSecretEngine(secret_engine); + cy.get('#secret_engine').should('have.class', 'ng-valid'); + cy.get('#address').type(address); + cy.get('#address').should('have.class', 'ng-valid'); + cy.contains('button', 'Submit').click(); + this.getFirstTableCell('SSE_KMS').should('exist'); + } + + edit(new_address: string) { + this.navigateEdit('SSE_KMS', true, false); + cy.get('#address').clear().type(new_address); + cy.get('#address').should('have.class', 'ng-valid'); + cy.get('#kms_provider').should('be.disabled'); + cy.contains('button', 'Submit').click(); + this.getTableCell(this.columnIndex.address, new_address) + .parent() + .find(`datatable-body-cell:nth-child(${this.columnIndex.address})`) + .should(($elements) => { + const address = $elements.text(); + expect(address).to.eq(new_address); + }); + } + + private selectKmsProvider(provider: string) { + return this.selectOption('kms_provider', provider); + } + + private selectAuthMethod(auth_method: string) { + return this.selectOption('auth_method', auth_method); + } + + private selectSecretEngine(secret_engine: string) { + return this.selectOption('secret_engine', secret_engine); + } +} 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 index e4f81f643c445..5dd7c51de6b46 100644 --- 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 @@ -1,7 +1,37 @@ -export class RgwBucketEncryptionModel { - kmsProviders = ['vault']; - authMethods = ['token', 'agent']; - secretEngines = ['kv', 'transit']; - sse_s3 = 'AES256'; - sse_kms = 'aws:kms'; +enum KmsProviders { + Vault = 'vault' } + +enum AuthMethods { + Token = 'token', + Agent = 'agent' +} + +enum SecretEngines { + KV = 'kv', + Transit = 'transit' +} + +enum sseS3 { + SSE_S3 = 'AES256' +} + +enum sseKms { + SSE_KMS = 'aws:kms' +} + +interface RgwBucketEncryptionModel { + kmsProviders: KmsProviders[]; + authMethods: AuthMethods[]; + secretEngines: SecretEngines[]; + SSE_S3: sseS3; + SSE_KMS: sseKms; +} + +export const rgwBucketEncryptionModel: RgwBucketEncryptionModel = { + kmsProviders: [KmsProviders.Vault], + authMethods: [AuthMethods.Token, AuthMethods.Agent], + secretEngines: [SecretEngines.KV, SecretEngines.Transit], + SSE_S3: sseS3.SSE_S3, + SSE_KMS: sseKms.SSE_KMS +}; diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.html index b25d47fecf33a..f77526be779b2 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.html @@ -296,12 +296,11 @@ name="encryption_enabled" formControlName="encryption_enabled" type="checkbox" - [attr.disabled]="!kmsVaultConfig && !s3VaultConfig ? true : null"/> + [attr.disabled]="!kmsConfigured && !s3Configured ? true : null"/> 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 Click here @@ -317,10 +316,11 @@ type="radio" name="encryption_type" value="AES256" - [attr.disabled]="!s3VaultConfig ? true : null"> + [attr.disabled]="!s3Configured ? true : null"> + i18n>SSE-S3 @@ -333,9 +333,10 @@ id="kms_enabled" name="encryption_type" value="aws:kms" - [attr.disabled]="!kmsVaultConfig ? true : null" + [attr.disabled]="!kmsConfigured ? true : null" type="radio"> diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.ts index cc2b5206517e0..d82c71e3cf74f 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.ts @@ -25,7 +25,7 @@ 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 { rgwBucketEncryptionModel } from '../models/rgw-bucket-encryption'; import { RgwBucketMfaDelete } from '../models/rgw-bucket-mfa-delete'; import { AclPermissionsType, @@ -33,7 +33,6 @@ import { RgwBucketAclGrantee as Grantee } from './rgw-bucket-acl-permissions.enum'; import { RgwBucketVersioning } from '../models/rgw-bucket-versioning'; -import { RgwConfigModalComponent } from '../rgw-config-modal/rgw-config-modal.component'; import { BucketTagModalComponent } from '../bucket-tag-modal/bucket-tag-modal.component'; import { TextAreaJsonFormatterService } from '~/app/shared/services/text-area-json-formatter.service'; import { RgwMultisiteService } from '~/app/shared/api/rgw-multisite.service'; @@ -44,8 +43,7 @@ import { TextAreaXmlFormatterService } from '~/app/shared/services/text-area-xml @Component({ selector: 'cd-rgw-bucket-form', templateUrl: './rgw-bucket-form.component.html', - styleUrls: ['./rgw-bucket-form.component.scss'], - providers: [RgwBucketEncryptionModel] + styleUrls: ['./rgw-bucket-form.component.scss'] }) export class RgwBucketFormComponent extends CdForm implements OnInit, AfterViewChecked { @ViewChild('bucketPolicyTextArea') @@ -64,8 +62,8 @@ export class RgwBucketFormComponent extends CdForm implements OnInit, AfterViewC isVersioningAlreadyEnabled = false; isMfaDeleteAlreadyEnabled = false; icons = Icons; - kmsVaultConfig = false; - s3VaultConfig = false; + kmsConfigured = false; + s3Configured = false; tags: Record[] = []; dirtyTags = false; tagConfig = [ @@ -97,7 +95,6 @@ export class RgwBucketFormComponent extends CdForm implements OnInit, AfterViewC private modalService: ModalService, private rgwUserService: RgwUserService, private notificationService: NotificationService, - private rgwEncryptionModal: RgwBucketEncryptionModel, private textAreaJsonFormatterService: TextAreaJsonFormatterService, private textAreaXmlFormatterService: TextAreaXmlFormatterService, public actionLabels: ActionLabelsI18n, @@ -187,15 +184,20 @@ export class RgwBucketFormComponent extends CdForm implements OnInit, AfterViewC ) ); - this.kmsProviders = this.rgwEncryptionModal.kmsProviders; + this.kmsProviders = rgwBucketEncryptionModel.kmsProviders; this.rgwBucketService.getEncryptionConfig().subscribe((data) => { - this.kmsVaultConfig = data[0]; - this.s3VaultConfig = data[1]; - if (this.kmsVaultConfig && this.s3VaultConfig) { + if (data['SSE_KMS']?.length > 0) { + this.kmsConfigured = true; + } + if (data['SSE_S3']?.length > 0) { + this.s3Configured = true; + } + // Set the encryption type based on the configurations + if (this.kmsConfigured && this.s3Configured) { this.bucketForm.get('encryption_type').setValue(''); - } else if (this.kmsVaultConfig) { + } else if (this.kmsConfigured) { this.bucketForm.get('encryption_type').setValue('aws:kms'); - } else if (this.s3VaultConfig) { + } else if (this.s3Configured) { this.bucketForm.get('encryption_type').setValue('AES256'); } else { this.bucketForm.get('encryption_type').setValue(''); @@ -459,13 +461,6 @@ export class RgwBucketFormComponent extends CdForm implements OnInit, AfterViewC this.bucketForm.updateValueAndValidity(); } - openConfigModal() { - const modalRef = this.modalService.show(RgwConfigModalComponent, null, { size: 'lg' }); - modalRef.componentInstance.configForm - .get('encryptionType') - .setValue(this.bucketForm.getValue('encryption_type') || 'AES256'); - } - showTagModal(index?: number) { const modalRef = this.modalService.show(BucketTagModalComponent); const modalComponent = modalRef.componentInstance as BucketTagModalComponent; diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-config-details/rgw-config-details.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-config-details/rgw-config-details.component.html new file mode 100644 index 0000000000000..ed79ed27b6053 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-config-details/rgw-config-details.component.html @@ -0,0 +1,17 @@ + + +
+
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-config-details/rgw-config-details.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-config-details/rgw-config-details.component.scss new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-config-details/rgw-config-details.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-config-details/rgw-config-details.component.spec.ts new file mode 100644 index 0000000000000..8f522560f341d --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-config-details/rgw-config-details.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { RgwConfigDetailsComponent } from './rgw-config-details.component'; + +describe('RgwConfigDetailsComponent', () => { + let component: RgwConfigDetailsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [RgwConfigDetailsComponent] + }).compileComponents(); + + fixture = TestBed.createComponent(RgwConfigDetailsComponent); + 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-details/rgw-config-details.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-config-details/rgw-config-details.component.ts new file mode 100644 index 0000000000000..689330f3cc49e --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-config-details/rgw-config-details.component.ts @@ -0,0 +1,37 @@ +import { Component, Input, OnChanges } from '@angular/core'; +import { rgwEncryptionConfigKeys } from '~/app/shared/models/rgw-encryption-config-keys'; + +@Component({ + selector: 'cd-rgw-config-details', + templateUrl: './rgw-config-details.component.html', + styleUrls: ['./rgw-config-details.component.scss'] +}) +export class RgwConfigDetailsComponent implements OnChanges { + transformedData: {}; + @Input() + selection: any; + + @Input() + excludeProps: any[] = []; + filteredEncryptionConfigValues: {}; + + ngOnChanges(): void { + if (this.selection) { + this.filteredEncryptionConfigValues = Object.keys(this.selection) + .filter((key) => !this.excludeProps.includes(key)) + .reduce((obj, key) => { + obj[key] = this.selection[key]; + return obj; + }, {}); + const transformedData = {}; + for (const key in this.filteredEncryptionConfigValues) { + if (rgwEncryptionConfigKeys[key]) { + transformedData[rgwEncryptionConfigKeys[key]] = this.filteredEncryptionConfigValues[key]; + } else { + transformedData[key] = this.filteredEncryptionConfigValues[key]; + } + } + this.transformedData = transformedData; + } + } +} 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 index a8ed178383476..7205665a7a72e 100644 --- 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 @@ -1,6 +1,6 @@ Update RGW Encryption Configurations + class="modal-title">{{ action | titlecase }} RGW Encryption Configurations
+ i18n>SSE-S3
@@ -28,11 +31,14 @@ formControlName="encryptionType" id="kmsEnabled" name="encryptionType" + (change)="checkKmsProviders()" value="aws:kms" + [attr.disabled]="editing && configForm.getValue('encryptionType') !== 'aws:kms' ? true : null" type="radio"> + i18n>SSE-KMS
@@ -46,9 +52,12 @@ name="kms_provider" class="form-select" formControlName="kms_provider"> - + + @@ -59,168 +68,170 @@ -
-
- -
- - This field is required. +
+
+
+ +
+ + This field is required. +
-
-
-
- -
- - This field is required. +
+
+ +
+ + This field is required. +
-
-
-
- -
- - This field is required. +
+
+ +
+ + This field is required. +
-
-
-
- -
- +
+
+ +
+ +
-
-
-
- -
- - This field is required. +
+
+ +
+ + This field is required. +
-
- -
- -
- - This field is required. -
-
-
-
-
-
-
- -
- - This field is required. +
+
+ +
+ + This field is required. +
-
-
-
- -
- - This field is required. +
+
+ +
+ + This field is required. +
+
+
+ +
+
+ +
+ + This field is required. +
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 index f2a0959109fbb..892916e86b5ea 100644 --- 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 @@ -12,13 +12,12 @@ 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'; +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] + styleUrls: ['./rgw-config-modal.component.scss'] }) export class RgwConfigModalComponent implements OnInit { readonly vaultAddress = /^((https?:\/\/)|(www.))(?:([a-zA-Z]+)|(\d+\.\d+.\d+.\d+)):\d{4}$/; @@ -32,21 +31,75 @@ export class RgwConfigModalComponent implements OnInit { authMethods: string[]; secretEngines: string[]; + selectedEncryptionConfigValues: any = {}; + allEncryptionConfigValues: any = []; + editing = false; + action: 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; + this.kmsProviders = rgwBucketEncryptionModel.kmsProviders; + this.authMethods = rgwBucketEncryptionModel.authMethods; + this.secretEngines = rgwBucketEncryptionModel.secretEngines; + if (this.editing && this.selectedEncryptionConfigValues) { + const patchValues = { + address: this.selectedEncryptionConfigValues['addr'], + encryptionType: + rgwBucketEncryptionModel[this.selectedEncryptionConfigValues['encryption_type']], + kms_provider: this.selectedEncryptionConfigValues['backend'], + auth_method: this.selectedEncryptionConfigValues['auth'], + secret_engine: this.selectedEncryptionConfigValues['secret_engine'], + secret_path: this.selectedEncryptionConfigValues['prefix'], + namespace: this.selectedEncryptionConfigValues['namespace'] + }; + this.configForm.patchValue(patchValues); + this.configForm.get('kms_provider').disable(); + } + this.checkKmsProviders(); + } + + checkKmsProviders() { + this.kmsProviders = rgwBucketEncryptionModel.kmsProviders; + if ( + this.allEncryptionConfigValues && + this.allEncryptionConfigValues.hasOwnProperty('SSE_KMS') && + !this.editing + ) { + const sseKmsBackends = this.allEncryptionConfigValues['SSE_KMS'].map( + (config: any) => config.backend + ); + if (this.configForm.get('encryptionType').value === rgwBucketEncryptionModel.SSE_KMS) { + this.kmsProviders = this.kmsProviders.filter( + (provider) => !sseKmsBackends.includes(provider) + ); + } + } + if ( + this.allEncryptionConfigValues && + this.allEncryptionConfigValues.hasOwnProperty('SSE_S3') && + !this.editing + ) { + const sseS3Backends = this.allEncryptionConfigValues['SSE_S3'].map( + (config: any) => config.backend + ); + if (this.configForm.get('encryptionType').value === rgwBucketEncryptionModel.SSE_S3) { + this.kmsProviders = this.kmsProviders.filter( + (provider) => !sseS3Backends.includes(provider) + ); + } + } + if (this.kmsProviders.length > 0 && !this.kmsProviders.includes('vault')) { + this.configForm.get('kms_provider').setValue(''); + } } createForm() { @@ -98,7 +151,7 @@ export class RgwConfigModalComponent implements OnInit { } onSubmit() { - const values = this.configForm.value; + const values = this.configForm.getRawValue(); this.rgwBucketService .setEncryptionConfig( values['encryptionType'], diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-configuration-page/rgw-configuration-page.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-configuration-page/rgw-configuration-page.component.html new file mode 100644 index 0000000000000..c33c8dbe4aac5 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-configuration-page/rgw-configuration-page.component.html @@ -0,0 +1,32 @@ + + +
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-configuration-page/rgw-configuration-page.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-configuration-page/rgw-configuration-page.component.scss new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-configuration-page/rgw-configuration-page.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-configuration-page/rgw-configuration-page.component.spec.ts new file mode 100644 index 0000000000000..a487050e91c4f --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-configuration-page/rgw-configuration-page.component.spec.ts @@ -0,0 +1,28 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { RgwConfigurationPageComponent } from './rgw-configuration-page.component'; +import { NgbActiveModal, NgbNavModule } from '@ng-bootstrap/ng-bootstrap'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { SharedModule } from '~/app/shared/shared.module'; +import { RgwModule } from '../rgw.module'; + +describe('RgwConfigurationPageComponent', () => { + let component: RgwConfigurationPageComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [RgwConfigurationPageComponent], + providers: [NgbActiveModal], + imports: [HttpClientTestingModule, SharedModule, NgbNavModule, RgwModule] + }).compileComponents(); + + fixture = TestBed.createComponent(RgwConfigurationPageComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-configuration-page/rgw-configuration-page.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-configuration-page/rgw-configuration-page.component.ts new file mode 100644 index 0000000000000..12e1a365200d4 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-configuration-page/rgw-configuration-page.component.ts @@ -0,0 +1,148 @@ +import { Component, EventEmitter, OnInit, Output } from '@angular/core'; + +import { NgbActiveModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; +import _ from 'lodash'; + +import { Permissions } from '~/app/shared/models/permissions'; + +import { RgwBucketService } from '~/app/shared/api/rgw-bucket.service'; +import { ActionLabelsI18n } from '~/app/shared/constants/app.constants'; +import { CdFormGroup } from '~/app/shared/forms/cd-form-group'; +import { CdTableColumn } from '~/app/shared/models/cd-table-column'; +import { CdTableSelection } from '~/app/shared/models/cd-table-selection'; +import { AuthStorageService } from '~/app/shared/services/auth-storage.service'; +import { ListWithDetails } from '~/app/shared/classes/list-with-details.class'; +import { CdTableAction } from '~/app/shared/models/cd-table-action'; +import { Icons } from '~/app/shared/enum/icons.enum'; +import { ModalService } from '~/app/shared/services/modal.service'; +import { RgwConfigModalComponent } from '../rgw-config-modal/rgw-config-modal.component'; +import { rgwBucketEncryptionModel } from '../models/rgw-bucket-encryption'; + +@Component({ + selector: 'cd-rgw-configuration-page', + templateUrl: './rgw-configuration-page.component.html', + styleUrls: ['./rgw-configuration-page.component.scss'] +}) +export class RgwConfigurationPageComponent extends ListWithDetails implements OnInit { + readonly vaultAddress = /^((https?:\/\/)|(www.))(?:([a-zA-Z]+)|(\d+\.\d+.\d+.\d+)):\d{4}$/; + + kmsProviders: string[]; + + columns: Array = []; + + configForm: CdFormGroup; + permissions: Permissions; + encryptionConfigValues: any = []; + selection: CdTableSelection = new CdTableSelection(); + + @Output() + submitAction = new EventEmitter(); + authMethods: string[]; + secretEngines: string[]; + tableActions: CdTableAction[]; + bsModalRef: NgbModalRef; + filteredEncryptionConfigValues: {}; + excludeProps: any[] = []; + disableCreate = false; + allEncryptionValues: any; + + constructor( + public activeModal: NgbActiveModal, + public actionLabels: ActionLabelsI18n, + private rgwBucketService: RgwBucketService, + public authStorageService: AuthStorageService, + private modalService: ModalService + ) { + super(); + this.permissions = this.authStorageService.getPermissions(); + } + + ngOnInit() { + this.columns = [ + { + name: $localize`Encryption Type`, + prop: 'encryption_type', + flexGrow: 1 + }, + { + name: $localize`Key Management Service Provider`, + prop: 'backend', + flexGrow: 1 + }, + { + name: $localize`Address`, + prop: 'addr', + flexGrow: 1 + } + ]; + this.tableActions = [ + { + permission: 'create', + icon: Icons.add, + name: this.actionLabels.CREATE, + click: () => this.openRgwConfigModal(false), + disable: () => this.disableCreate + }, + { + permission: 'update', + icon: Icons.edit, + name: this.actionLabels.EDIT, + click: () => this.openRgwConfigModal(true) + } + ]; + + this.rgwBucketService.getEncryptionConfig().subscribe((data: any) => { + this.allEncryptionValues = data; + const allowedBackends = rgwBucketEncryptionModel.kmsProviders; + + const kmsBackends = this.getBackend(data, 'SSE_KMS'); + const s3Backends = this.getBackend(data, 'SSE_S3'); + + const allKmsBackendsPresent = this.areAllAllowedBackendsPresent(allowedBackends, kmsBackends); + const allS3BackendsPresent = this.areAllAllowedBackendsPresent(allowedBackends, s3Backends); + + this.disableCreate = allKmsBackendsPresent && allS3BackendsPresent; + this.encryptionConfigValues = Object.values(data).flat(); + }); + + this.excludeProps = this.columns.map((column) => column.prop); + this.excludeProps.push('unique_id'); + } + + getBackend(encryptionData: { [x: string]: any[] }, encryptionType: string) { + return new Set(encryptionData[encryptionType].map((item) => item.backend)); + } + + areAllAllowedBackendsPresent(allowedBackends: any[], backendsSet: Set) { + return allowedBackends.every((backend) => backendsSet.has(backend)); + } + + openRgwConfigModal(edit: boolean) { + if (edit) { + const initialState = { + action: 'edit', + editing: true, + selectedEncryptionConfigValues: this.selection.first() + }; + this.bsModalRef = this.modalService.show(RgwConfigModalComponent, initialState, { + size: 'lg' + }); + } else { + const initialState = { + action: 'create', + allEncryptionConfigValues: this.allEncryptionValues + }; + this.bsModalRef = this.modalService.show(RgwConfigModalComponent, initialState, { + size: 'lg' + }); + } + } + + updateSelection(selection: CdTableSelection) { + this.selection = selection; + } + + setExpandedRow(expandedRow: any) { + super.setExpandedRow(expandedRow); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts index 803e3c5bdf83d..dde6cff4866be 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts @@ -56,6 +56,8 @@ import { NfsListComponent } from '../nfs/nfs-list/nfs-list.component'; import { NfsFormComponent } from '../nfs/nfs-form/nfs-form.component'; import { RgwMultisiteSyncPolicyComponent } from './rgw-multisite-sync-policy/rgw-multisite-sync-policy.component'; import { RgwMultisiteSyncPolicyFormComponent } from './rgw-multisite-sync-policy-form/rgw-multisite-sync-policy-form.component'; +import { RgwConfigurationPageComponent } from './rgw-configuration-page/rgw-configuration-page.component'; +import { RgwConfigDetailsComponent } from './rgw-config-details/rgw-config-details.component'; @NgModule({ imports: [ @@ -116,7 +118,9 @@ import { RgwMultisiteSyncPolicyFormComponent } from './rgw-multisite-sync-policy RgwSyncDataInfoComponent, BucketTagModalComponent, RgwMultisiteSyncPolicyComponent, - RgwMultisiteSyncPolicyFormComponent + RgwMultisiteSyncPolicyFormComponent, + RgwConfigDetailsComponent, + RgwConfigurationPageComponent ], providers: [TitleCasePipe] }) @@ -253,6 +257,11 @@ const routes: Routes = [ data: { breadcrumbs: ActionLabels.EDIT } } ] + }, + { + path: 'configuration', + data: { breadcrumbs: 'Configuration' }, + children: [{ path: '', component: RgwConfigurationPageComponent }] } ]; diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.html index f238946963d58..c6464fe5e4449 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.html @@ -225,6 +225,11 @@ i18n-title *ngIf="permissions.nfs.read && enabledFeature.nfs" class="tc_submenuitem tc_submenuitem_rgw_nfs">NFS + Configuration List[str]: + pass + + @abstractmethod + def get_required_keys(self) -> List[str]: + pass + + @abstractmethod + def get_key_pattern(self, enc_type: str) -> str: + pass + + +class VaultConfig(BackendConfig): + def get_config_keys(self) -> List[str]: + return ['addr', 'auth', 'namespace', 'prefix', 'secret_engine', + 'token_file', 'ssl_cacert', 'ssl_clientcert', 'ssl_clientkey', + 'verify_ssl'] + + def get_required_keys(self) -> List[str]: + return ['auth', 'prefix', 'secret_engine', 'addr'] + + def get_key_pattern(self, enc_type: str) -> str: + return 'rgw_crypt_{backend}_{key}' if enc_type == 'SSE_KMS' else 'rgw_crypt_sse_s3_{backend}_{key}' # noqa E501 #pylint: disable=line-too-long + + +class KmipConfig(BackendConfig): + def get_config_keys(self) -> List[str]: + return ['addr', 'ca_path', 'client_cert', 'client_key', 'kms_key_template', + 'password', 's3_key_template', 'username'] + + def get_required_keys(self) -> List[str]: + return ['addr', 'username', 'password'] + + def get_key_pattern(self, enc_type: str) -> str: + return 'rgw_crypt_{backend}_{key}' if enc_type == 'SSE_KMS' else 'rgw_crypt_sse_s3_{backend}_{key}' # noqa E501 #pylint: disable=line-too-long + + # pylint: disable=too-many-public-methods class CephService(object): @@ -183,64 +223,59 @@ class CephService(object): return None @classmethod - def get_encryption_config(cls, daemon_name): - kms_vault_configured = False - s3_vault_configured = False - kms_backend: str = '' - sse_s3_backend: str = '' - vault_stats = [] - full_daemon_name = 'rgw.' + daemon_name + def get_encryption_config(cls, daemon_name: str) -> Dict[str, List[Dict[str, Any]]]: + # Define backends with their respective configuration classes + backends: Dict[str, Dict[str, BackendConfig]] = { + 'SSE_KMS': { + 'vault': VaultConfig(), + 'kmip': KmipConfig() + }, + 'SSE_S3': { + 'vault': VaultConfig() + } + } - kms_backend = CephService.send_command('mon', 'config get', - who=name_to_config_section(full_daemon_name), - key='rgw_crypt_s3_kms_backend') - sse_s3_backend = CephService.send_command('mon', 'config get', - who=name_to_config_section(full_daemon_name), - 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(full_daemon_name), # noqa E501 #pylint: disable=line-too-long - key='rgw_crypt_vault_auth') - kms_vault_engine: str = CephService.send_command('mon', 'config get', - who=name_to_config_section(full_daemon_name), # noqa E501 #pylint: disable=line-too-long - key='rgw_crypt_vault_secret_engine') - kms_vault_address: str = CephService.send_command('mon', 'config get', - who=name_to_config_section(full_daemon_name), # noqa E501 #pylint: disable=line-too-long - key='rgw_crypt_vault_addr') - kms_vault_token: str = CephService.send_command('mon', 'config get', - who=name_to_config_section(full_daemon_name), # noqa E501 #pylint: disable=line-too-long - key='rgw_crypt_vault_token_file') # noqa E501 #pylint: disable=line-too-long - if (kms_vault_auth.strip() != "" and kms_vault_engine.strip() != "" and kms_vault_address.strip() != ""): # noqa E501 #pylint: disable=line-too-long - if(kms_vault_auth == 'token' and kms_vault_token.strip() == ""): - kms_vault_configured = False - else: - 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(full_daemon_name), # noqa E501 #pylint: disable=line-too-long - key='rgw_crypt_sse_s3_vault_auth') - s3_vault_engine: str = CephService.send_command('mon', - 'config get', - who=name_to_config_section(full_daemon_name), # noqa E501 #pylint: disable=line-too-long - 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(full_daemon_name), # noqa E501 #pylint: disable=line-too-long - key='rgw_crypt_sse_s3_vault_addr') - s3_vault_token: str = CephService.send_command('mon', 'config get', - who=name_to_config_section(full_daemon_name), # noqa E501 #pylint: disable=line-too-long - key='rgw_crypt_sse_s3_vault_token_file') # noqa E501 #pylint: disable=line-too-long - - if (s3_vault_auth.strip() != "" and s3_vault_engine.strip() != "" and s3_vault_address.strip() != ""): # noqa E501 #pylint: disable=line-too-long - if(s3_vault_auth == 'token' and s3_vault_token.strip() == ""): - s3_vault_configured = False - else: - s3_vault_configured = True + # Final configuration values + config_values: Dict[str, List[Dict[str, Any]]] = { + 'SSE_KMS': [], + 'SSE_S3': [] + } + + full_daemon_name = 'rgw.' + daemon_name - vault_stats.append(kms_vault_configured) - vault_stats.append(s3_vault_configured) - return vault_stats + for enc_type, backend_list in backends.items(): + for backend_name, backend in backend_list.items(): + config_keys = backend.get_config_keys() + required_keys = backend.get_required_keys() + key_pattern = backend.get_key_pattern(enc_type) + + # Check if all required configurations are present and not empty + all_required_configs_present = True + for key in required_keys: + config_key = key_pattern.format(backend=backend_name, key=key) + value = CephService.send_command('mon', 'config get', + who=name_to_config_section(full_daemon_name), + key=config_key) + if not (isinstance(value, str) and value.strip()): + all_required_configs_present = False + break + + # If all required configurations are present, gather all config values + if all_required_configs_present: + config_dict = {} + for key in config_keys: + config_key = key_pattern.format(backend=backend_name, key=key) + value = CephService.send_command('mon', 'config get', + who=name_to_config_section(full_daemon_name), # noqa E501 #pylint: disable=line-too-long + key=config_key) + if value: + config_dict[key] = value.strip() if isinstance(value, str) else value + config_dict['backend'] = backend_name + config_dict['encryption_type'] = enc_type + config_dict['unique_id'] = enc_type + '-' + backend_name + config_values[enc_type].append(config_dict) + + return config_values @classmethod def set_encryption_config(cls, encryption_type, kms_provider, auth_method,