rgw_client = RgwClient.admin_instance()
return rgw_client.get_bucket_policy(bucket)
+ def _set_policy(self, bucket_name: str, policy: str, daemon_name, owner):
+ rgw_client = RgwClient.instance(owner, daemon_name)
+ return rgw_client.set_bucket_policy(bucket_name, policy)
+
def _set_tags(self, bucket_name, tags, daemon_name, owner):
rgw_client = RgwClient.instance(owner, daemon_name)
return rgw_client.set_tags(bucket_name, tags)
result['encryption'] = encryption['Status']
result['versioning'] = versioning['Status']
result['mfa_delete'] = versioning['MfaDelete']
- result['policy'] = self._get_policy(bucket_name)
+ result['bucket_policy'] = self._get_policy(bucket_name)
# Append the locking configuration.
locking = self._get_locking(result['owner'], daemon_name, bucket_name)
lock_enabled='false', lock_mode=None,
lock_retention_period_days=None,
lock_retention_period_years=None, encryption_state='false',
- encryption_type=None, key_id=None, tags=None, daemon_name=None):
+ encryption_type=None, key_id=None, tags=None,
+ bucket_policy=None, daemon_name=None):
lock_enabled = str_to_bool(lock_enabled)
encryption_state = str_to_bool(encryption_state)
try:
if tags:
self._set_tags(bucket, tags, daemon_name, uid)
+ if bucket_policy:
+ self._set_policy(bucket, bucket_policy, daemon_name, uid)
+
return result
except RequestException as e: # pragma: no cover - handling is too obvious
raise DashboardException(e, http_status_code=500, component='rgw')
encryption_state='false', encryption_type=None, key_id=None,
mfa_delete=None, mfa_token_serial=None, mfa_token_pin=None,
lock_mode=None, lock_retention_period_days=None,
- lock_retention_period_years=None, tags=None, daemon_name=None):
+ lock_retention_period_years=None, tags=None, bucket_policy=None, daemon_name=None):
encryption_state = str_to_bool(encryption_state)
# When linking a non-tenant-user owned bucket to a tenanted user, we
# need to prefix bucket name with '/'. e.g. photos -> /photos
self._delete_encryption(bucket_name, daemon_name, uid)
if tags:
self._set_tags(bucket_name, tags, daemon_name, uid)
+ if bucket_policy:
+ self._set_policy(bucket, bucket_policy, daemon_name, uid)
return self._append_bid(result)
def delete(self, bucket, purge_objects='true', daemon_name=None):
</table>
<!-- Tags -->
- <ng-container *ngIf="selection.tagset">
+ <ng-container *ngIf="(selection.tagset | keyvalue)?.length">
<legend i18n>Tags</legend>
<table class="table table-striped table-bordered">
<tbody>
<ng-container ngbNavItem="permissions">
<a ngbNavLink
- i18n>Permissions</a>
+ i18n>Policies</a>
<ng-template ngbNavContent>
<table class="table table-striped table-bordered">
<tbody>
<tr>
<td i18n
- class="bold w-25">Policy</td>
- <td><pre>{{ selection.policy | json}}</pre></td>
+ class="bold w-25">Bucket policy</td>
+ <td><pre>{{ selection.bucket_policy | json}}</pre></td>
</tr>
</tbody>
</table>
this.rgwBucketService.get(this.selection.bid).subscribe((bucket: object) => {
bucket['lock_retention_period_days'] = this.rgwBucketService.getLockDays(bucket);
this.selection = bucket;
- this.selection.policy = JSON.parse(this.selection.policy) || {};
+ this.selection.bucket_policy = JSON.parse(this.selection.bucket_policy) || {};
});
}
}
</div>
</div>
+ <!-- Policies -->
+ <legend class="cd-header"
+ i18n>Policies
+ </legend>
+ <div class="row">
+ <div class="col-12">
+ <div class="form-group row">
+ <label i18n
+ class="cd-col-form-label"
+ for="id">Bucket policy</label>
+ <div class="cd-col-form-input">
+ <textarea #bucketPolicyTextArea
+ class="form-control resize-vertical"
+ id="bucket_policy"
+ formControlName="bucket_policy"
+ (change)="bucketPolicyOnChange()">
+ </textarea>
+ <span class="invalid-feedback"
+ *ngIf="bucketForm.showError('bucket_policy', frm, 'invalidJson')"
+ i18n>Invalid json text</span>
+ <div class="btn-group float-end"
+ role="group"
+ aria-label="bucket-policy-helpers">
+ <button type="button"
+ id="example-generator-button"
+ class="btn btn-light my-3"
+ (click)="openUrl('https://docs.aws.amazon.com/AmazonS3/latest/userguide/example-bucket-policies.html?icmpid=docs_amazons3_console')"
+ i18n>
+ <i [ngClass]="[icons.externalUrl]"></i>
+ Policy examples
+ </button>
+ <button type="button"
+ id="example-generator-button"
+ class="btn btn-light my-3"
+ (click)="openUrl('https://awspolicygen.s3.amazonaws.com/policygen.html')"
+ i18n>
+ <i [ngClass]="[icons.externalUrl]"></i>
+ Policy generator
+ </button>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
</div>
<div class="card-footer">
-import { AfterViewChecked, ChangeDetectorRef, Component, OnInit } from '@angular/core';
+import {
+ AfterViewChecked,
+ ChangeDetectorRef,
+ Component,
+ OnInit,
+ ViewChild,
+ ElementRef
+} from '@angular/core';
import { AbstractControl, Validators } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
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';
@Component({
selector: 'cd-rgw-bucket-form',
providers: [RgwBucketEncryptionModel]
})
export class RgwBucketFormComponent extends CdForm implements OnInit, AfterViewChecked {
+ @ViewChild('bucketPolicyTextArea')
+ public bucketPolicyTextArea: ElementRef<any>;
+
bucketForm: CdFormGroup;
editing = false;
owners: string[] = null;
private rgwUserService: RgwUserService,
private notificationService: NotificationService,
private rgwEncryptionModal: RgwBucketEncryptionModel,
+ private textAreaJsonFormatterService: TextAreaJsonFormatterService,
public actionLabels: ActionLabelsI18n,
private readonly changeDetectorRef: ChangeDetectorRef
) {
ngAfterViewChecked(): void {
this.changeDetectorRef.detectChanges();
+ this.bucketPolicyOnChange();
}
createForm() {
]
],
lock_mode: ['COMPLIANCE'],
- lock_retention_period_days: [0, [CdValidators.number(false), lockDaysValidator]]
+ lock_retention_period_days: [0, [CdValidators.number(false), lockDaysValidator]],
+ bucket_policy: ['{}', CdValidators.json()]
});
}
if (value['lock_enabled']) {
this.bucketForm.controls['versioning'].disable();
}
+ if (value['bucket_policy']) {
+ this.bucketForm
+ .get('bucket_policy')
+ .setValue(JSON.stringify(value['bucket_policy'], null, 2));
+ }
}
}
this.loadingReady();
}
const values = this.bucketForm.value;
const xmlStrTags = this.tagsToXML(this.tags);
+ const bucketPolicy = this.getBucketPolicy();
if (this.editing) {
// Edit
const versioning = this.getVersioningStatus();
values['mfa-token-pin'],
values['lock_mode'],
values['lock_retention_period_days'],
- xmlStrTags
+ xmlStrTags,
+ bucketPolicy
)
.subscribe(
() => {
values['encryption_enabled'],
values['encryption_type'],
values['keyId'],
- xmlStrTags
+ xmlStrTags,
+ bucketPolicy
)
.subscribe(
() => {
return this.isMfaDeleteEnabled ? RgwBucketMfaDelete.ENABLED : RgwBucketMfaDelete.DISABLED;
}
+ getBucketPolicy() {
+ return this.bucketForm.getValue('bucket_policy') || '{}';
+ }
+
fileUpload(files: FileList, controlName: string) {
const file: File = files[0];
const reader = new FileReader();
});
}
+ bucketPolicyOnChange() {
+ if (this.bucketPolicyTextArea) {
+ this.textAreaJsonFormatterService.format(this.bucketPolicyTextArea);
+ }
+ }
+
+ openUrl(url: string) {
+ window.open(url, '_blank');
+ }
+
openConfigModal() {
const modalRef = this.modalService.show(RgwConfigModalComponent, null, { size: 'lg' });
modalRef.componentInstance.configForm
deleteTag(index: number) {
this.tags.splice(index, 1);
+ this.bucketForm.markAsDirty();
+ this.bucketForm.updateValueAndValidity();
}
private setTag(tag: Record<string, string>, index?: number) {
true,
'aws:kms',
'qwerty1',
+ null,
null
)
.subscribe();
const req = httpTesting.expectOne(
- `api/rgw/bucket?bucket=foo&uid=bar&zonegroup=default&placement_target=default-placement&lock_enabled=false&lock_mode=COMPLIANCE&lock_retention_period_days=5&encryption_state=true&encryption_type=aws%253Akms&key_id=qwerty1&tags=null&${RgwHelper.DAEMON_QUERY_PARAM}`
+ `api/rgw/bucket?bucket=foo&uid=bar&zonegroup=default&placement_target=default-placement&lock_enabled=false&lock_mode=COMPLIANCE&lock_retention_period_days=5&encryption_state=true&encryption_type=aws%253Akms&key_id=qwerty1&tags=null&bucket_policy=null&${RgwHelper.DAEMON_QUERY_PARAM}`
);
expect(req.request.method).toBe('POST');
});
'223344',
'GOVERNANCE',
'10',
+ null,
null
)
.subscribe();
const req = httpTesting.expectOne(
- `api/rgw/bucket/foo?${RgwHelper.DAEMON_QUERY_PARAM}&bucket_id=bar&uid=baz&versioning_state=Enabled&encryption_state=true&encryption_type=aws%253Akms&key_id=qwerty1&mfa_delete=Enabled&mfa_token_serial=1&mfa_token_pin=223344&lock_mode=GOVERNANCE&lock_retention_period_days=10&tags=null`
+ `api/rgw/bucket/foo?${RgwHelper.DAEMON_QUERY_PARAM}&bucket_id=bar&uid=baz&versioning_state=Enabled&encryption_state=true&encryption_type=aws%253Akms&key_id=qwerty1&mfa_delete=Enabled&mfa_token_serial=1&mfa_token_pin=223344&lock_mode=GOVERNANCE&lock_retention_period_days=10&tags=null&bucket_policy=null`
);
expect(req.request.method).toBe('PUT');
});
encryption_state: boolean,
encryption_type: string,
key_id: string,
- tags: string
+ tags: string,
+ bucketPolicy: string
) {
return this.rgwDaemonService.request((params: HttpParams) => {
return this.http.post(this.url, null, {
encryption_type,
key_id,
tags: tags,
+ bucket_policy: bucketPolicy,
daemon_name: params.get('daemon_name')
}
})
mfaTokenPin: string,
lockMode: 'GOVERNANCE' | 'COMPLIANCE',
lockRetentionPeriodDays: string,
- tags: string
+ tags: string,
+ bucketPolicy: string
) {
return this.rgwDaemonService.request((params: HttpParams) => {
params = params.appendAll({
mfa_token_pin: mfaTokenPin,
lock_mode: lockMode,
lock_retention_period_days: lockRetentionPeriodDays,
- tags: tags
+ tags: tags,
+ bucket_policy: bucketPolicy
});
return this.http.put(`${this.url}/${bucket}`, null, { params: params });
});
);
};
}
+
+ static json(): ValidatorFn {
+ return (control: AbstractControl): Record<string, any> | null => {
+ if (!control.value) return null;
+ try {
+ JSON.parse(control.value);
+ return null;
+ } catch (e) {
+ return { invalidJson: true };
+ }
+ };
+ }
}
import { Component, ViewChild, ElementRef } from '@angular/core';
import { FieldType, FieldTypeConfig } from '@ngx-formly/core';
+import { TextAreaJsonFormatterService } from '~/app/shared/services/text-area-json-formatter.service';
@Component({
selector: 'cd-formly-textarea-type',
@ViewChild('textArea')
public textArea: ElementRef<any>;
+ constructor(private textAreaJsonFormatterService: TextAreaJsonFormatterService) {
+ super();
+ }
+
onChange() {
- const value = this.textArea.nativeElement.value;
- try {
- const formatted = JSON.stringify(JSON.parse(value), null, 2);
- this.textArea.nativeElement.value = formatted;
- this.textArea.nativeElement.style.height = 'auto';
- const lineNumber = formatted.split('\n').length;
- const pixelPerLine = 25;
- const pixels = lineNumber * pixelPerLine;
- this.textArea.nativeElement.style.height = pixels + 'px';
- } catch (e) {}
+ this.textAreaJsonFormatterService.format(this.textArea);
}
}
--- /dev/null
+import { TestBed } from '@angular/core/testing';
+
+import { TextAreaJsonFormatterService } from './text-area-json-formatter.service';
+
+describe('TextAreaJsonFormatterService', () => {
+ let service: TextAreaJsonFormatterService;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({});
+ service = TestBed.inject(TextAreaJsonFormatterService);
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+});
--- /dev/null
+import { ElementRef, Injectable } from '@angular/core';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class TextAreaJsonFormatterService {
+ constructor() {}
+
+ format(textArea: ElementRef<any>): void {
+ const value = textArea.nativeElement.value;
+ try {
+ const formatted = JSON.stringify(JSON.parse(value), null, 2);
+ textArea.nativeElement.value = formatted;
+ textArea.nativeElement.style.height = 'auto';
+ const lineNumber = formatted.split('\n').length;
+ const pixelPerLine = 20;
+ const pixels = lineNumber * pixelPerLine;
+ textArea.nativeElement.style.height = pixels + 'px';
+ } catch (e) {}
+ }
+}
properties:
bucket:
type: string
+ bucket_policy:
+ type: string
daemon_name:
type: string
encryption_state:
properties:
bucket_id:
type: string
+ bucket_policy:
+ type: string
daemon_name:
type: string
encryption_state:
return None
raise e
+ @RestClient.api_put('/{bucket_name}?policy')
+ def set_bucket_policy(self, bucket_name: str, policy: str, request=None):
+ """
+ Sets the bucket policy for a bucket.
+ :param bucket_name: The name of the bucket.
+ :type bucket_name: str
+ :param policy: The bucket policy.
+ :type policy: JSON Structured Document
+ :return: The bucket policy.
+ :rtype: Dict
+ """
+ # pylint: disable=unused-argument
+ try:
+ request = request(data=policy)
+ except RequestException as e:
+ if e.content:
+ content = json_str_to_object(e.content)
+ if content.get("Code") == "InvalidArgument":
+ msg = "Invalid JSON document"
+ raise DashboardException(msg=msg, component='rgw')
+ raise DashboardException(e)
+
def perform_validations(self, retention_period_days, retention_period_years, mode):
try:
retention_period_days = int(retention_period_days) if retention_period_days else 0