Merge pull request #58114 from rhcs-dashboard/add-configuration-page-rgw

mgr/dashboard: add a new configuration page in side nav bar  Object > Configuration

Reviewed-by: Ankush Behl <cloudbehl@gmail.com>
Reviewed-by: Nizamudeen A <nia@redhat.com>
This commit is contained in:
Nizamudeen A 2024-07-08 13:27:16 +05:30 committed by GitHub
commit 7789fcc6c2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 783 additions and 264 deletions

View File

@ -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);

View File

@ -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);

View File

@ -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');
});
});
});

View File

@ -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);
}
}

View File

@ -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
};

View File

@ -296,12 +296,11 @@
name="encryption_enabled"
formControlName="encryption_enabled"
type="checkbox"
[attr.disabled]="!kmsVaultConfig && !s3VaultConfig ? true : null"/>
[attr.disabled]="!kmsConfigured && !s3Configured ? true : null"/>
<cd-help-text aria-label="encryption 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()"
To set the configuration values <a href="#/rgw/configuration"
aria-label="click here">Click here</a></span>
</cd-help-text>
</div>
@ -317,10 +316,11 @@
type="radio"
name="encryption_type"
value="AES256"
[attr.disabled]="!s3VaultConfig ? true : null">
[attr.disabled]="!s3Configured ? true : null">
<label class="form-control-label"
[ngClass]="{'text-muted': !s3Configured}"
for="sse_S3_enabled"
i18n>SSE-S3 Encryption</label>
i18n>SSE-S3</label>
</div>
</div>
</div>
@ -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">
<label class="form-control-label"
[ngClass]="{'text-muted': !kmsConfigured}"
for="kms_enabled"
i18n>Connect to an external key management service</label>
</div>

View File

@ -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<string, string>[] = [];
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;

View File

@ -0,0 +1,17 @@
<ng-container *ngIf="selection">
<nav ngbNav
#nav="ngbNav"
id="tabset-config-details"
class="nav-tabs"
cdStatefulTab="config-details">
<ng-container ngbNavItem="details">
<a ngbNavLink
i18n>Details</a>
<ng-template ngbNavContent>
<cd-table-key-value [data]="transformedData">
</cd-table-key-value>
</ng-template>
</ng-container>
</nav>
<div [ngbNavOutlet]="nav"></div>
</ng-container>

View File

@ -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<RgwConfigDetailsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [RgwConfigDetailsComponent]
}).compileComponents();
fixture = TestBed.createComponent(RgwConfigDetailsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -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;
}
}
}

View File

@ -1,6 +1,6 @@
<cd-modal [modalRef]="activeModal">
<ng-container i18n="form title"
class="modal-title">Update RGW Encryption Configurations</ng-container>
class="modal-title">{{ action | titlecase }} RGW Encryption Configurations</ng-container>
<ng-container class="modal-content">
<form name="configForm"
@ -17,10 +17,13 @@
id="s3Enabled"
type="radio"
name="encryptionType"
(change)="checkKmsProviders()"
[attr.disabled]="editing && configForm.getValue('encryptionType') !== 'AES256' ? true : null"
value="AES256">
<label class="custom-check-label"
[ngClass]="{'text-muted': editing && configForm.getValue('encryptionType') !== 'AES256'}"
for="s3Enabled"
i18n>SSE-S3 Encryption</label>
i18n>SSE-S3</label>
</div>
<div class="col-md-auto custom-checkbox form-check-inline">
@ -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">
<label class="custom-check-label"
[ngClass]="{'text-muted': editing && configForm.getValue('encryptionType') !== 'aws:kms'}"
for="kmsEnabled"
i18n>SSE-KMS Encryption</label>
i18n>SSE-KMS</label>
</div>
</div>
@ -46,9 +52,12 @@
name="kms_provider"
class="form-select"
formControlName="kms_provider">
<option i18n
*ngIf="kmsProviders !== null"
[ngValue]="null">-- Select a provider --</option>
<option *ngIf="kmsProviders !== null && kmsProviders.length === 0"
ngValue="null"
i18n>-- No kms providers available --</option>
<option *ngIf="kmsProviders !== null && kmsProviders.length > 0"
ngValue=""
i18n>-- Select a provider --</option>
<option *ngFor="let provider of kmsProviders"
[value]="provider">{{ provider }}</option>
</select>
@ -59,168 +68,170 @@
</div>
</div>
<div *ngIf="configForm.getValue('encryptionType') === 'aws:kms' || configForm.getValue('encryptionType') === 'AES256'">
<div class="form-group row">
<div *ngIf="kmsProviders.length !== 0 && configForm.getValue('kms_provider') !== ''">
<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-select"
formControlName="auth_method">
<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-select"
formControlName="secret_engine">
<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"
placeholder="http://127.0.0.1:8000">
<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="auth_method"
i18n>Authentication Method</label>
<div class="cd-col-form-input">
<select id="auth_method"
name="auth_method"
class="form-select"
formControlName="auth_method">
<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-select"
formControlName="secret_engine">
<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"
placeholder="http://127.0.0.1:8000">
<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>
<cd-helper i18n>
The token authentication method expects a Vault token to be present in a plaintext file.
</cd-helper>
</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('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>
for="token">
<span i18n>Token</span>
<cd-helper i18n>
The token authentication method expects a Vault token to be present in a plaintext file.
</cd-helper>
</label>
<div class="cd-col-form-input">
<input type="file"
formControlName="ssl_cert"
(change)="fileUpload($event.target.files, 'ssl_cert')">
formControlName="token"
(change)="fileUpload($event.target.files, 'token')">
<span class="invalid-feedback"
*ngIf="configForm.showError('ssl_cert', frm, 'required')"
*ngIf="configForm.showError('token', 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 *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>
<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 *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>

View File

@ -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'],

View File

@ -0,0 +1,32 @@
<nav ngbNav
#nav="ngbNav"
class="nav-tabs">
<ng-container ngbNavItem>
<a ngbNavLink
i18n>Server-side Encryption</a>
<ng-template ngbNavContent>
<cd-table #table
[data]="encryptionConfigValues"
[columns]="columns"
identifier="unique_id"
[forceIdentifier]="true"
[hasDetails]="true"
(updateSelection)="updateSelection($event)"
(setExpandedRow)="setExpandedRow($event)"
columnMode="flex"
selectionType="single">
<cd-table-actions class="table-actions"
[permission]="permissions.configOpt"
[selection]="selection"
[tableActions]="tableActions">
</cd-table-actions>
<cd-rgw-config-details cdTableDetail
[selection]="expandedRow"
[excludeProps]="excludeProps">
</cd-rgw-config-details>
</cd-table>
</ng-template>
</ng-container>
</nav>
<div [ngbNavOutlet]="nav"></div>

View File

@ -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<RgwConfigurationPageComponent>;
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();
});
});

View File

@ -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<CdTableColumn> = [];
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<any>) {
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);
}
}

View File

@ -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 }]
}
];

View File

@ -229,6 +229,11 @@
i18n-title
*ngIf="permissions.nfs.read && enabledFeature.nfs"
class="tc_submenuitem tc_submenuitem_rgw_nfs"><span i18n>NFS</span></cds-sidenav-item>
<cds-sidenav-item route="/rgw/configuration"
[useRouter]="true"
title="Configuration"
i18n-title
class="tc_submenuitem tc_submenuitem_rgw_configuration"><span i18n>Configuration</span></cds-sidenav-item>
</cds-sidenav-menu>
<!-- Filesystem -->
<cds-sidenav-menu title="File"

View File

@ -0,0 +1,21 @@
export enum rgwEncryptionConfigKeys {
auth = 'Authentication Method',
encryption_type = 'Encryption Type',
backend = 'Backend',
prefix = 'Prefix',
namespace = 'Namespace',
secret_engine = 'Secret Engine',
addr = 'Address',
token_file = 'Token File',
ssl_cacert = 'SSL CA Certificate',
ssl_clientcert = 'SSL Client Certificate',
ssl_clientkey = 'SSL Client Key',
verify_ssl = 'Verify SSL',
ca_path = 'CA Path',
client_cert = 'Client Certificate',
client_key = 'Client Key',
kms_key_template = 'KMS Key Template',
password = 'Password',
s3_key_template = 'S3 Key Template',
username = 'Username'
}

View File

@ -2,6 +2,7 @@
import json
import logging
from abc import ABC, abstractmethod
import rados
from mgr_module import CommandResult
@ -10,7 +11,7 @@ from mgr_util import get_most_recent_rate, get_time_series_rates, name_to_config
from .. import mgr
try:
from typing import Any, Dict, Optional, Union
from typing import Any, Dict, List, Optional, Union
except ImportError:
pass # For typing only
@ -24,6 +25,45 @@ class SendCommandError(rados.Error):
super(SendCommandError, self).__init__(err, errno)
class BackendConfig(ABC):
@abstractmethod
def get_config_keys(self) -> 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 = []
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()
}
}
# Final configuration values
config_values: Dict[str, List[Dict[str, Any]]] = {
'SSE_KMS': [],
'SSE_S3': []
}
full_daemon_name = 'rgw.' + daemon_name
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')
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)
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
# 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 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 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)
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
vault_stats.append(kms_vault_configured)
vault_stats.append(s3_vault_configured)
return vault_stats
return config_values
@classmethod
def set_encryption_config(cls, encryption_type, kms_provider, auth_method,