# -*- coding: utf-8 -*-
from __future__ import absolute_import
+import base64
import logging
+import time
import urllib
+from cryptography.hazmat.backends import default_backend
+from cryptography.hazmat.primitives.twofactor.totp import TOTP
+from cryptography.hazmat.primitives.hashes import SHA1
+
from .helper import DashboardTestCase, JObj, JList, JLeaf
logger = logging.getLogger(__name__)
# Delete administrator account.
cls._radosgw_admin_cmd(['user', 'rm', '--uid', 'admin'])
if cls.create_test_user:
- cls._radosgw_admin_cmd(['user', 'rm', '--uid=teuth-test-user'])
+ cls._radosgw_admin_cmd(['user', 'rm', '--uid=teuth-test-user', '--purge-data'])
super(RgwTestCase, cls).tearDownClass()
- def setUp(self):
- super(RgwTestCase, self).setUp()
-
def get_rgw_user(self, uid):
return self._get('/api/rgw/user/{}'.format(uid))
class RgwBucketTest(RgwTestCase):
+ _mfa_token_serial = '1'
+ _mfa_token_seed = '23456723'
+ _mfa_token_time_step = 1
+
AUTH_ROLES = ['rgw-manager']
@classmethod
def setUpClass(cls):
cls.create_test_user = True
super(RgwBucketTest, cls).setUpClass()
+ # Create MFA TOTP token for test user.
+ cls._radosgw_admin_cmd([
+ 'mfa', 'create', '--uid', 'teuth-test-user', '--totp-serial', cls._mfa_token_serial,
+ '--totp-seed', cls._mfa_token_seed, '--totp-seed-type', 'base32',
+ '--totp-seconds', str(cls._mfa_token_time_step), '--totp-window', '2'
+ ])
# Create tenanted users.
cls._radosgw_admin_cmd([
'user', 'create', '--tenant', 'testx', '--uid', 'teuth-test-user',
@classmethod
def tearDownClass(cls):
cls._radosgw_admin_cmd(
- ['user', 'rm', '--tenant', 'testx', '--uid=teuth-test-user'])
+ ['user', 'rm', '--tenant', 'testx', '--uid=teuth-test-user', '--purge-data'])
cls._radosgw_admin_cmd(
- ['user', 'rm', '--tenant', 'testx2', '--uid=teuth-test-user2'])
+ ['user', 'rm', '--tenant', 'testx2', '--uid=teuth-test-user2', '--purge-data'])
super(RgwBucketTest, cls).tearDownClass()
+ def _get_mfa_token_pin(self):
+ totp_key = base64.b32decode(self._mfa_token_seed)
+ totp = TOTP(totp_key, 6, SHA1(), self._mfa_token_time_step, backend=default_backend(),
+ enforce_key_length=False)
+ time_value = time.time()
+ return totp.generate(time_value)
+
def test_all(self):
# Create a new bucket.
self._post(
self.assertEqual(data['placement_rule'], 'default-placement')
self.assertEqual(data['versioning'], 'Suspended')
- # Update the bucket.
+ # Update bucket: change owner, enable versioning.
self._put(
'/api/rgw/bucket/teuth-test-bucket',
params={
self.assertEqual(data['owner'], 'teuth-test-user')
self.assertEqual(data['versioning'], 'Enabled')
+ # Update bucket: enable MFA Delete.
+ self._put(
+ '/api/rgw/bucket/teuth-test-bucket',
+ params={
+ 'bucket_id': data['id'],
+ 'uid': 'teuth-test-user',
+ 'versioning_state': 'Enabled',
+ 'mfa_delete': 'Enabled',
+ 'mfa_token_serial': self._mfa_token_serial,
+ 'mfa_token_pin': self._get_mfa_token_pin()
+ })
+ self.assertStatus(200)
+ data = self._get('/api/rgw/bucket/teuth-test-bucket')
+ self.assertStatus(200)
+ self.assertEqual(data['versioning'], 'Enabled')
+ self.assertEqual(data['mfa_delete'], 'Enabled')
+
+ # Update bucket: disable versioning & MFA Delete.
+ time.sleep(self._mfa_token_time_step) # Required to get new TOTP pin.
+ self._put(
+ '/api/rgw/bucket/teuth-test-bucket',
+ params={
+ 'bucket_id': data['id'],
+ 'uid': 'teuth-test-user',
+ 'versioning_state': 'Suspended',
+ 'mfa_delete': 'Disabled',
+ 'mfa_token_serial': self._mfa_token_serial,
+ 'mfa_token_pin': self._get_mfa_token_pin()
+ })
+ self.assertStatus(200)
+ data = self._get('/api/rgw/bucket/teuth-test-bucket')
+ self.assertStatus(200)
+ self.assertEqual(data['versioning'], 'Suspended')
+ self.assertEqual(data['mfa_delete'], 'Disabled')
+
# Delete the bucket.
self._delete('/api/rgw/bucket/teuth-test-bucket')
self.assertStatus(204)
rgw_client = RgwClient.instance(owner)
return rgw_client.get_bucket_versioning(bucket_name)
- def _set_versioning(self, owner, bucket_name, versioning_state):
- rgw_client = RgwClient.instance(owner)
- return rgw_client.set_bucket_versioning(bucket_name, versioning_state)
+ def _set_versioning(self, owner, bucket_name, versioning_state, mfa_delete,
+ mfa_token_serial, mfa_token_pin):
+ bucket_versioning = self._get_versioning(owner, bucket_name)
+ if versioning_state != bucket_versioning['Status']\
+ or (mfa_delete and mfa_delete != bucket_versioning['MfaDelete']):
+ rgw_client = RgwClient.instance(owner)
+ rgw_client.set_bucket_versioning(bucket_name, versioning_state, mfa_delete,
+ mfa_token_serial, mfa_token_pin)
@staticmethod
def strip_tenant_from_bucket_name(bucket_name):
# type: (str) -> dict
result = self.proxy('GET', 'bucket', {'bucket': bucket})
- result['versioning'] =\
+ bucket_versioning =\
self._get_versioning(result['owner'],
RgwBucket.get_s3_bucket_name(result['bucket'], result['tenant']))
+ result['versioning'] = bucket_versioning['Status']
+ result['mfa_delete'] = bucket_versioning['MfaDelete']
return self._append_bid(result)
except RequestException as e:
raise DashboardException(e, http_status_code=500, component='rgw')
- def set(self, bucket, bucket_id, uid, versioning_state=None):
+ def set(self, bucket, bucket_id, uid, versioning_state=None, mfa_delete=None,
+ mfa_token_serial=None, mfa_token_pin=None):
# 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:
uid_tenant = uid[:uid.find('$')] if uid.find('$') >= 0 else None
self._set_versioning(uid,
RgwBucket.get_s3_bucket_name(bucket, uid_tenant),
- versioning_state)
+ versioning_state, mfa_delete, mfa_token_serial, mfa_token_pin)
return self._append_bid(result)
await this.selectOwner(new_owner);
// Enable versioning
- await expect(element(by.css('input[name=versioning]:checked')).getAttribute('value')).toBe(
- this.versioningStateSuspended
- );
- await element(by.css('input[id=enabled]')).click();
- await expect(element(by.css('input[name=versioning]:checked')).getAttribute('value')).toBe(
- this.versioningStateEnabled
- );
+ await expect(element(by.css('input[id=versioning]')).getAttribute('checked')).toBeFalsy();
+ await element(by.css('label[for=versioning]')).click();
+ await expect(element(by.css('input[id=versioning]')).getAttribute('checked')).toBeTruthy();
await element(by.cssContainingText('button', 'Edit Bucket')).click();
await this.waitClickableAndClick(this.getFirstTableCellWithText(name)); // wait for table to load and click
await element(by.cssContainingText('button', 'Edit')).click(); // click button to move to edit page
await this.waitTextToBePresent(this.getBreadcrumb(), 'Edit');
- await element(by.css('input[id=suspended]')).click();
- await expect(element(by.css('input[name=versioning]:checked')).getAttribute('value')).toBe(
- this.versioningStateSuspended
- );
+ await element(by.css('label[for=versioning]')).click();
+ await expect(element(by.css('input[id=versioning]')).getAttribute('checked')).toBeFalsy();
await element(by.cssContainingText('button', 'Edit Bucket')).click();
// Check versioning suspended:
await this.waitTextToBePresent(this.getBreadcrumb(), 'Edit');
- await expect(element(by.css('input[name=versioning]:checked')).getAttribute('value')).toBe(
- this.versioningStateSuspended
- );
+ await expect(element(by.css('input[id=versioning]')).getAttribute('checked')).toBeFalsy();
// Chooses 'Select a user' rather than a valid owner on Edit Bucket page
// and checks if it's an invalid input
--- /dev/null
+export enum RgwBucketMfaDelete {
+ ENABLED = 'Enabled',
+ DISABLED = 'Disabled'
+}
--- /dev/null
+export enum RgwBucketVersioning {
+ ENABLED = 'Enabled',
+ SUSPENDED = 'Suspended'
+}
class="bold">Versioning</td>
<td>{{ bucket.versioning }}</td>
</tr>
+ <tr>
+ <td i18n
+ class="bold">MFA Delete</td>
+ <td>{{ bucket.mfa_delete }}</td>
+ </tr>
</tbody>
</table>
</div>
<!-- Versioning -->
- <div class="form-group row"
- *ngIf="editing">
- <legend class="cd-header ml-5 mr-5"
- i18n>Versioning</legend>
- <div class="cd-col-form-offset">
- <input type="radio"
- id="enabled"
- name="versioning"
- formControlName="versioning"
- value="Enabled"
- [checked]="bucketForm.get('versioning').value == 'Enabled'"
- class="custom-control custom-radio custom-control-inline align-top">
- <label class="align-text-top"
- for="enabled">
- <span i18n>Enabled</span>
- <div class="text-muted"
- i18n>Enables versioning for the objects in the bucket.</div>
- </label>
+ <legend class="cd-header"
+ i18n>Versioning</legend>
+
+ <ng-container *ngIf="editing">
+ <div class="form-group row">
+ <div class="cd-col-form-offset">
+ <div class="custom-control custom-checkbox">
+ <input type="checkbox"
+ class="custom-control-input"
+ id="versioning"
+ name="versioning"
+ formControlName="versioning"
+ [checked]="isVersioningEnabled"
+ (change)="updateVersioning()">
+ <label class="custom-control-label"
+ for="versioning"
+ i18n>Versioning enabled</label>
+ <cd-helper>
+ <span i18n>Enables versioning for the objects in the bucket.</span>
+ </cd-helper>
+ </div>
+ </div>
</div>
- <div class="cd-col-form-offset">
- <input type="radio"
- id="suspended"
- name="versioning"
- formControlName="versioning"
- value="Suspended"
- [checked]="bucketForm.get('versioning').value != 'Enabled'"
- class="custom-control custom-radio custom-control-inline align-top">
- <label class="align-text-top"
- for="suspended">
- <span i18n>Suspended</span>
- <div class="text-muted"
- i18n>Disables versioning for the objects in the bucket.</div>
- </label>
- <span class="invalid-feedback"
- *ngIf="bucketForm.showError('versioning', frm, 'required')"
- i18n>This field is required.</span>
+
+ <!-- MFA Delete -->
+ <legend class="cd-header"
+ i18n>Multi-Factor Authentication</legend>
+
+ <div class="form-group row">
+ <div class="cd-col-form-offset">
+ <div class="custom-control custom-checkbox">
+ <input type="checkbox"
+ class="custom-control-input"
+ id="mfa-delete"
+ name="mfa-delete"
+ formControlName="mfa-delete"
+ [checked]="isMfaDeleteEnabled"
+ (change)="updateMfaDelete()">
+ <label class="custom-control-label"
+ for="mfa-delete"
+ i18n>Delete enabled</label>
+ <cd-helper>
+ <span i18n>Enables MFA (multi-factor authentication) Delete, which requires additional authentication for changing the bucket versioning state.</span>
+ </cd-helper>
+ </div>
+ </div>
</div>
- </div>
+ <div *ngIf="areMfaCredentialsRequired()"
+ class="form-group row">
+ <label i18n
+ class="cd-col-form-label"
+ for="mfa-token-serial">Token Serial Number</label>
+ <div class="cd-col-form-input">
+ <input type="text"
+ id="mfa-token-serial"
+ name="mfa-token-serial"
+ formControlName="mfa-token-serial"
+ class="form-control">
+ <span class="invalid-feedback"
+ *ngIf="bucketForm.showError('mfa-token-serial', frm, 'required')"
+ i18n>This field is required.</span>
+ </div>
+ </div>
+ <div *ngIf="areMfaCredentialsRequired()"
+ class="form-group row">
+ <label i18n
+ class="cd-col-form-label"
+ for="mfa-token-pin">Token PIN</label>
+ <div class="cd-col-form-input">
+ <input type="text"
+ id="mfa-token-pin"
+ name="mfa-token-pin"
+ formControlName="mfa-token-pin"
+ class="form-control">
+ <span class="invalid-feedback"
+ *ngIf="bucketForm.showError('mfa-token-pin', frm, 'required')"
+ i18n>This field is required.</span>
+ </div>
+ </div>
+ </ng-container>
+
</div>
<div class="card-footer">
import { NotificationType } from '../../../shared/enum/notification-type.enum';
import { NotificationService } from '../../../shared/services/notification.service';
import { SharedModule } from '../../../shared/shared.module';
+import { RgwBucketMfaDelete } from '../models/rgw-bucket-mfa-delete';
+import { RgwBucketVersioning } from '../models/rgw-bucket-versioning';
import { RgwBucketFormComponent } from './rgw-bucket-form.component';
describe('RgwBucketFormComponent', () => {
let fixture: ComponentFixture<RgwBucketFormComponent>;
let rgwBucketService: RgwBucketService;
let getPlacementTargetsSpy: jasmine.Spy;
+ let rgwBucketServiceGetSpy: jasmine.Spy;
configureTestBed({
declarations: [RgwBucketFormComponent],
fixture = TestBed.createComponent(RgwBucketFormComponent);
component = fixture.componentInstance;
rgwBucketService = TestBed.get(RgwBucketService);
+ rgwBucketServiceGetSpy = spyOn(rgwBucketService, 'get');
getPlacementTargetsSpy = spyOn(TestBed.get(RgwSiteService), 'getPlacementTargets');
});
);
});
});
+
+ describe('mfa credentials', () => {
+ const checkMfaCredentialsVisibility = (
+ fakeResponse: object,
+ versioningChecked: boolean,
+ mfaDeleteChecked: boolean,
+ expectedVisibility: boolean
+ ) => {
+ component['route'].params = observableOf({ bid: 'bid' });
+ component.editing = true;
+ rgwBucketServiceGetSpy.and.returnValue(observableOf(fakeResponse));
+ component.ngOnInit();
+ component.isVersioningEnabled = versioningChecked;
+ component.isMfaDeleteEnabled = mfaDeleteChecked;
+ fixture.detectChanges();
+
+ const mfaTokenSerial = fixture.debugElement.nativeElement.querySelector('#mfa-token-serial');
+ const mfaTokenPin = fixture.debugElement.nativeElement.querySelector('#mfa-token-pin');
+ if (expectedVisibility) {
+ expect(mfaTokenSerial).toBeTruthy();
+ expect(mfaTokenPin).toBeTruthy();
+ } else {
+ expect(mfaTokenSerial).toBeFalsy();
+ expect(mfaTokenPin).toBeFalsy();
+ }
+ };
+
+ it('inputs should be visible when required', () => {
+ checkMfaCredentialsVisibility(
+ {
+ versioning: RgwBucketVersioning.SUSPENDED,
+ mfa_delete: RgwBucketMfaDelete.DISABLED
+ },
+ false,
+ false,
+ false
+ );
+ checkMfaCredentialsVisibility(
+ {
+ versioning: RgwBucketVersioning.SUSPENDED,
+ mfa_delete: RgwBucketMfaDelete.DISABLED
+ },
+ true,
+ false,
+ false
+ );
+ checkMfaCredentialsVisibility(
+ {
+ versioning: RgwBucketVersioning.ENABLED,
+ mfa_delete: RgwBucketMfaDelete.DISABLED
+ },
+ false,
+ false,
+ false
+ );
+ checkMfaCredentialsVisibility(
+ {
+ versioning: RgwBucketVersioning.ENABLED,
+ mfa_delete: RgwBucketMfaDelete.ENABLED
+ },
+ true,
+ true,
+ false
+ );
+ checkMfaCredentialsVisibility(
+ {
+ versioning: RgwBucketVersioning.SUSPENDED,
+ mfa_delete: RgwBucketMfaDelete.DISABLED
+ },
+ false,
+ true,
+ true
+ );
+ checkMfaCredentialsVisibility(
+ {
+ versioning: RgwBucketVersioning.SUSPENDED,
+ mfa_delete: RgwBucketMfaDelete.ENABLED
+ },
+ false,
+ false,
+ true
+ );
+ checkMfaCredentialsVisibility(
+ {
+ versioning: RgwBucketVersioning.SUSPENDED,
+ mfa_delete: RgwBucketMfaDelete.ENABLED
+ },
+ true,
+ true,
+ true
+ );
+ checkMfaCredentialsVisibility(
+ {
+ versioning: RgwBucketVersioning.ENABLED,
+ mfa_delete: RgwBucketMfaDelete.ENABLED
+ },
+ false,
+ true,
+ true
+ );
+ });
+ });
});
import { RgwSiteService } from '../../../shared/api/rgw-site.service';
import { RgwUserService } from '../../../shared/api/rgw-user.service';
import { ActionLabelsI18n, URLVerbs } from '../../../shared/constants/app.constants';
+import { Icons } from '../../../shared/enum/icons.enum';
import { NotificationType } from '../../../shared/enum/notification-type.enum';
import { CdFormBuilder } from '../../../shared/forms/cd-form-builder';
import { CdFormGroup } from '../../../shared/forms/cd-form-group';
import { CdValidators } from '../../../shared/forms/cd-validators';
import { NotificationService } from '../../../shared/services/notification.service';
+import { RgwBucketMfaDelete } from '../models/rgw-bucket-mfa-delete';
+import { RgwBucketVersioning } from '../models/rgw-bucket-versioning';
@Component({
selector: 'cd-rgw-bucket-form',
resource: string;
zonegroup: string;
placementTargets: object[] = [];
+ isVersioningEnabled = false;
+ isVersioningAlreadyEnabled = false;
+ isMfaDeleteEnabled = false;
+ isMfaDeleteAlreadyEnabled = false;
+ icons = Icons;
constructor(
private route: ActivatedRoute,
bid: [null, [Validators.required], this.editing ? [] : [this.bucketNameValidator()]],
owner: [null, [Validators.required]],
'placement-target': [null, this.editing ? [] : [Validators.required]],
- versioning: [null, this.editing ? [Validators.required] : []]
+ versioning: [null],
+ 'mfa-delete': [null],
+ 'mfa-token-serial': [''],
+ 'mfa-token-pin': ['']
});
}
value = _.merge(defaults, value);
// Update the form.
this.bucketForm.setValue(value);
+ if (this.editing) {
+ this.setVersioningStatus(resp['versioning']);
+ this.isVersioningAlreadyEnabled = this.isVersioningEnabled;
+ this.setMfaDeleteStatus(resp['mfa_delete']);
+ this.isMfaDeleteAlreadyEnabled = this.isMfaDeleteEnabled;
+ this.setMfaDeleteValidators();
+ }
});
});
}
if (this.editing) {
// Edit
const idCtl = this.bucketForm.get('id');
- const versioningCtl = this.bucketForm.get('versioning');
+ const versioning = this.getVersioningStatus();
+ const mfaDelete = this.getMfaDeleteStatus();
+ const mfaTokenSerial = this.bucketForm.getValue('mfa-token-serial');
+ const mfaTokenPin = this.bucketForm.getValue('mfa-token-pin');
this.rgwBucketService
- .update(bidCtl.value, idCtl.value, ownerCtl.value, versioningCtl.value)
+ .update(
+ bidCtl.value,
+ idCtl.value,
+ ownerCtl.value,
+ versioning,
+ mfaDelete,
+ mfaTokenSerial,
+ mfaTokenPin
+ )
.subscribe(
() => {
this.notificationService.show(
});
};
}
+
+ areMfaCredentialsRequired() {
+ return (
+ this.isMfaDeleteEnabled !== this.isMfaDeleteAlreadyEnabled ||
+ (this.isMfaDeleteAlreadyEnabled &&
+ this.isVersioningEnabled !== this.isVersioningAlreadyEnabled)
+ );
+ }
+
+ setMfaDeleteValidators() {
+ const mfaTokenSerialControl = this.bucketForm.get('mfa-token-serial');
+ const mfaTokenPinControl = this.bucketForm.get('mfa-token-pin');
+
+ if (this.areMfaCredentialsRequired()) {
+ mfaTokenSerialControl.setValidators(Validators.required);
+ mfaTokenPinControl.setValidators(Validators.required);
+ } else {
+ mfaTokenSerialControl.setValidators(null);
+ mfaTokenPinControl.setValidators(null);
+ }
+
+ mfaTokenSerialControl.updateValueAndValidity();
+ mfaTokenPinControl.updateValueAndValidity();
+ }
+
+ getVersioningStatus() {
+ return this.isVersioningEnabled ? RgwBucketVersioning.ENABLED : RgwBucketVersioning.SUSPENDED;
+ }
+
+ setVersioningStatus(status: RgwBucketVersioning) {
+ this.isVersioningEnabled = status === RgwBucketVersioning.ENABLED;
+ }
+
+ updateVersioning() {
+ this.isVersioningEnabled = !this.isVersioningEnabled;
+
+ this.setMfaDeleteValidators();
+ }
+
+ getMfaDeleteStatus() {
+ return this.isMfaDeleteEnabled ? RgwBucketMfaDelete.ENABLED : RgwBucketMfaDelete.DISABLED;
+ }
+
+ setMfaDeleteStatus(status: RgwBucketMfaDelete) {
+ this.isMfaDeleteEnabled = status === RgwBucketMfaDelete.ENABLED;
+ }
+
+ updateMfaDelete() {
+ this.isMfaDeleteEnabled = !this.isMfaDeleteEnabled;
+
+ this.setMfaDeleteValidators();
+ }
}
});
it('should call update', () => {
- service.update('foo', 'bar', 'baz', 'Enabled').subscribe();
+ service.update('foo', 'bar', 'baz', 'Enabled', 'Enabled', '1', '223344').subscribe();
const req = httpTesting.expectOne(
- 'api/rgw/bucket/foo?bucket_id=bar&uid=baz&versioning_state=Enabled'
+ 'api/rgw/bucket/foo?bucket_id=bar&uid=baz&versioning_state=Enabled&mfa_delete=Enabled&mfa_token_serial=1&mfa_token_pin=223344'
);
expect(req.request.method).toBe('PUT');
});
return this.http.post(this.url, null, { params: params });
}
- update(bucket: string, bucketId: string, uid: string, versioningState: string) {
+ update(
+ bucket: string,
+ bucketId: string,
+ uid: string,
+ versioningState: string,
+ mfaDelete: string,
+ mfaTokenSerial: string,
+ mfaTokenPin: string
+ ) {
let params = new 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);
return this.http.put(`${this.url}/${bucket}`, null, { params: params });
}
method=None,
params=None,
data=None,
- raw_content=False):
+ raw_content=False,
+ headers=None):
method = method if method else self.method
if not method:
raise Exception('No HTTP request method specified')
method.upper()))
data = req_data
resp = self.rest_client.do_request(method, self._gen_path(), params,
- data, raw_content)
+ data, raw_content, headers)
if raw_content and self.resp_structure:
raise Exception("Cannot validate response in raw format")
_ResponseValidator.validate(self.resp_structure, resp)
path,
params=None,
data=None,
- raw_content=False):
+ raw_content=False,
+ headers=None):
url = '{}{}'.format(self.base_url, path)
logger.debug('%s REST API %s req: %s data: %s', self.client_name,
method.upper(), path, data)
+ request_headers = self.headers.copy()
+ if headers:
+ request_headers.update(headers)
try:
if method.lower() == 'get':
resp = self.session.get(
- url, headers=self.headers, params=params, auth=self.auth)
+ url, headers=request_headers, params=params, auth=self.auth)
elif method.lower() == 'post':
resp = self.session.post(
url,
- headers=self.headers,
+ headers=request_headers,
params=params,
data=data,
auth=self.auth)
elif method.lower() == 'put':
resp = self.session.put(
url,
- headers=self.headers,
+ headers=request_headers,
params=params,
data=data,
auth=self.auth)
elif method.lower() == 'delete':
resp = self.session.delete(
url,
- headers=self.headers,
+ headers=request_headers,
params=params,
data=data,
auth=self.auth)
import xml.etree.ElementTree as ET # noqa: N814
import six
from ..awsauth import S3Auth
+from ..exceptions import DashboardException
from ..settings import Settings, Options
from ..rest_client import RestClient, RequestException
from ..tools import build_url, dict_contains_path, json_str_to_object, partial_dict
"""
Get bucket versioning.
:param str bucket_name: the name of the bucket.
- :return: versioning state
- :rtype: str
+ :return: versioning info
+ :rtype: Dict
"""
# pylint: disable=unused-argument
result = request()
if 'Status' not in result:
result['Status'] = 'Suspended'
- return result['Status']
+ if 'MfaDelete' not in result:
+ result['MfaDelete'] = 'Disabled'
+ return result
@RestClient.api_put('/{bucket_name}?versioning')
- def set_bucket_versioning(self, bucket_name, versioning_state, request=None):
+ def set_bucket_versioning(self, bucket_name, versioning_state, mfa_delete,
+ mfa_token_serial, mfa_token_pin, request=None):
"""
Set bucket versioning.
:param str bucket_name: the name of the bucket.
:param str versioning_state:
https://docs.aws.amazon.com/AmazonS3/latest/API/RESTBucketPUTVersioningStatus.html
+ :param str mfa_delete: MFA Delete state.
+ :param str mfa_token_serial:
+ https://docs.ceph.com/docs/master/radosgw/mfa/
+ :param str mfa_token_pin: value of a TOTP token at a certain time (auth code)
:return: None
"""
# pylint: disable=unused-argument
versioning_configuration = ET.Element('VersioningConfiguration')
- status = ET.SubElement(versioning_configuration, 'Status')
- status.text = versioning_state
+ status_element = ET.SubElement(versioning_configuration, 'Status')
+ status_element.text = versioning_state
+
+ headers = {}
+ if mfa_delete and mfa_token_serial and mfa_token_pin:
+ headers['x-amz-mfa'] = '{} {}'.format(mfa_token_serial, mfa_token_pin)
+ mfa_delete_element = ET.SubElement(versioning_configuration, 'MfaDelete')
+ mfa_delete_element.text = mfa_delete
+
data = ET.tostring(versioning_configuration, encoding='utf-8')
- return request(data=data)
+ try:
+ request(data=data, headers=headers)
+ except RequestException as error:
+ msg = str(error)
+ if error.status_code == 403:
+ msg = 'Bad MFA credentials: {}'.format(msg)
+ # Avoid dashboard GUI redirections caused by status code (403, ...):
+ http_status_code = 400 if 400 <= error.status_code < 500 else error.status_code
+ raise DashboardException(msg=msg,
+ http_status_code=http_status_code,
+ component='rgw')