Skip to content

Commit 8312655

Browse files
authored
Merge pull request ceph#55062 from rhcs-dashboard/set-bucket-policy
mgr/dashboard: set bucket policies Reviewed-by: Ankush Behl <cloudbehl@gmail.com> Reviewed-by: Nizamudeen A <nia@redhat.com>
2 parents e608006 + 2817d8e commit 8312655

File tree

14 files changed

+197
-27
lines changed

14 files changed

+197
-27
lines changed

src/pybind/mgr/dashboard/controllers/rgw.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,10 @@ def _get_policy(self, bucket: str):
291291
rgw_client = RgwClient.admin_instance()
292292
return rgw_client.get_bucket_policy(bucket)
293293

294+
def _set_policy(self, bucket_name: str, policy: str, daemon_name, owner):
295+
rgw_client = RgwClient.instance(owner, daemon_name)
296+
return rgw_client.set_bucket_policy(bucket_name, policy)
297+
294298
def _set_tags(self, bucket_name, tags, daemon_name, owner):
295299
rgw_client = RgwClient.instance(owner, daemon_name)
296300
return rgw_client.set_tags(bucket_name, tags)
@@ -347,7 +351,7 @@ def get(self, bucket, daemon_name=None):
347351
result['encryption'] = encryption['Status']
348352
result['versioning'] = versioning['Status']
349353
result['mfa_delete'] = versioning['MfaDelete']
350-
result['policy'] = self._get_policy(bucket_name)
354+
result['bucket_policy'] = self._get_policy(bucket_name)
351355

352356
# Append the locking configuration.
353357
locking = self._get_locking(result['owner'], daemon_name, bucket_name)
@@ -360,7 +364,8 @@ def create(self, bucket, uid, zonegroup=None, placement_target=None,
360364
lock_enabled='false', lock_mode=None,
361365
lock_retention_period_days=None,
362366
lock_retention_period_years=None, encryption_state='false',
363-
encryption_type=None, key_id=None, tags=None, daemon_name=None):
367+
encryption_type=None, key_id=None, tags=None,
368+
bucket_policy=None, daemon_name=None):
364369
lock_enabled = str_to_bool(lock_enabled)
365370
encryption_state = str_to_bool(encryption_state)
366371
try:
@@ -379,6 +384,9 @@ def create(self, bucket, uid, zonegroup=None, placement_target=None,
379384
if tags:
380385
self._set_tags(bucket, tags, daemon_name, uid)
381386

387+
if bucket_policy:
388+
self._set_policy(bucket, bucket_policy, daemon_name, uid)
389+
382390
return result
383391
except RequestException as e: # pragma: no cover - handling is too obvious
384392
raise DashboardException(e, http_status_code=500, component='rgw')
@@ -388,7 +396,7 @@ def set(self, bucket, bucket_id, uid, versioning_state=None,
388396
encryption_state='false', encryption_type=None, key_id=None,
389397
mfa_delete=None, mfa_token_serial=None, mfa_token_pin=None,
390398
lock_mode=None, lock_retention_period_days=None,
391-
lock_retention_period_years=None, tags=None, daemon_name=None):
399+
lock_retention_period_years=None, tags=None, bucket_policy=None, daemon_name=None):
392400
encryption_state = str_to_bool(encryption_state)
393401
# When linking a non-tenant-user owned bucket to a tenanted user, we
394402
# need to prefix bucket name with '/'. e.g. photos -> /photos
@@ -430,6 +438,8 @@ def set(self, bucket, bucket_id, uid, versioning_state=None,
430438
self._delete_encryption(bucket_name, daemon_name, uid)
431439
if tags:
432440
self._set_tags(bucket_name, tags, daemon_name, uid)
441+
if bucket_policy:
442+
self._set_policy(bucket, bucket_policy, daemon_name, uid)
433443
return self._append_bid(result)
434444

435445
def delete(self, bucket, purge_objects='true', daemon_name=None):

src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-details/rgw-bucket-details.component.html

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@
102102
</table>
103103

104104
<!-- Tags -->
105-
<ng-container *ngIf="selection.tagset">
105+
<ng-container *ngIf="(selection.tagset | keyvalue)?.length">
106106
<legend i18n>Tags</legend>
107107
<table class="table table-striped table-bordered">
108108
<tbody>
@@ -120,15 +120,15 @@
120120

121121
<ng-container ngbNavItem="permissions">
122122
<a ngbNavLink
123-
i18n>Permissions</a>
123+
i18n>Policies</a>
124124
<ng-template ngbNavContent>
125125

126126
<table class="table table-striped table-bordered">
127127
<tbody>
128128
<tr>
129129
<td i18n
130-
class="bold w-25">Policy</td>
131-
<td><pre>{{ selection.policy | json}}</pre></td>
130+
class="bold w-25">Bucket policy</td>
131+
<td><pre>{{ selection.bucket_policy | json}}</pre></td>
132132
</tr>
133133
</tbody>
134134
</table>

src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-details/rgw-bucket-details.component.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export class RgwBucketDetailsComponent implements OnChanges {
1818
this.rgwBucketService.get(this.selection.bid).subscribe((bucket: object) => {
1919
bucket['lock_retention_period_days'] = this.rgwBucketService.getLockDays(bucket);
2020
this.selection = bucket;
21-
this.selection.policy = JSON.parse(this.selection.policy) || {};
21+
this.selection.bucket_policy = JSON.parse(this.selection.bucket_policy) || {};
2222
});
2323
}
2424
}

src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.html

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,50 @@
410410
</div>
411411
</div>
412412

413+
<!-- Policies -->
414+
<legend class="cd-header"
415+
i18n>Policies
416+
</legend>
417+
<div class="row">
418+
<div class="col-12">
419+
<div class="form-group row">
420+
<label i18n
421+
class="cd-col-form-label"
422+
for="id">Bucket policy</label>
423+
<div class="cd-col-form-input">
424+
<textarea #bucketPolicyTextArea
425+
class="form-control resize-vertical"
426+
id="bucket_policy"
427+
formControlName="bucket_policy"
428+
(change)="bucketPolicyOnChange()">
429+
</textarea>
430+
<span class="invalid-feedback"
431+
*ngIf="bucketForm.showError('bucket_policy', frm, 'invalidJson')"
432+
i18n>Invalid json text</span>
433+
<div class="btn-group float-end"
434+
role="group"
435+
aria-label="bucket-policy-helpers">
436+
<button type="button"
437+
id="example-generator-button"
438+
class="btn btn-light my-3"
439+
(click)="openUrl('https://docs.aws.amazon.com/AmazonS3/latest/userguide/example-bucket-policies.html?icmpid=docs_amazons3_console')"
440+
i18n>
441+
<i [ngClass]="[icons.externalUrl]"></i>
442+
Policy examples
443+
</button>
444+
<button type="button"
445+
id="example-generator-button"
446+
class="btn btn-light my-3"
447+
(click)="openUrl('https://awspolicygen.s3.amazonaws.com/policygen.html')"
448+
i18n>
449+
<i [ngClass]="[icons.externalUrl]"></i>
450+
Policy generator
451+
</button>
452+
</div>
453+
</div>
454+
</div>
455+
</div>
456+
</div>
413457

414458
</div>
415459
<div class="card-footer">

src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.ts

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
import { AfterViewChecked, ChangeDetectorRef, Component, OnInit } from '@angular/core';
1+
import {
2+
AfterViewChecked,
3+
ChangeDetectorRef,
4+
Component,
5+
OnInit,
6+
ViewChild,
7+
ElementRef
8+
} from '@angular/core';
29
import { AbstractControl, Validators } from '@angular/forms';
310
import { ActivatedRoute, Router } from '@angular/router';
411

@@ -22,6 +29,7 @@ import { RgwBucketMfaDelete } from '../models/rgw-bucket-mfa-delete';
2229
import { RgwBucketVersioning } from '../models/rgw-bucket-versioning';
2330
import { RgwConfigModalComponent } from '../rgw-config-modal/rgw-config-modal.component';
2431
import { BucketTagModalComponent } from '../bucket-tag-modal/bucket-tag-modal.component';
32+
import { TextAreaJsonFormatterService } from '~/app/shared/services/text-area-json-formatter.service';
2533

2634
@Component({
2735
selector: 'cd-rgw-bucket-form',
@@ -30,6 +38,9 @@ import { BucketTagModalComponent } from '../bucket-tag-modal/bucket-tag-modal.co
3038
providers: [RgwBucketEncryptionModel]
3139
})
3240
export class RgwBucketFormComponent extends CdForm implements OnInit, AfterViewChecked {
41+
@ViewChild('bucketPolicyTextArea')
42+
public bucketPolicyTextArea: ElementRef<any>;
43+
3344
bucketForm: CdFormGroup;
3445
editing = false;
3546
owners: string[] = null;
@@ -70,6 +81,7 @@ export class RgwBucketFormComponent extends CdForm implements OnInit, AfterViewC
7081
private rgwUserService: RgwUserService,
7182
private notificationService: NotificationService,
7283
private rgwEncryptionModal: RgwBucketEncryptionModel,
84+
private textAreaJsonFormatterService: TextAreaJsonFormatterService,
7385
public actionLabels: ActionLabelsI18n,
7486
private readonly changeDetectorRef: ChangeDetectorRef
7587
) {
@@ -82,6 +94,7 @@ export class RgwBucketFormComponent extends CdForm implements OnInit, AfterViewC
8294

8395
ngAfterViewChecked(): void {
8496
this.changeDetectorRef.detectChanges();
97+
this.bucketPolicyOnChange();
8598
}
8699

87100
createForm() {
@@ -129,7 +142,8 @@ export class RgwBucketFormComponent extends CdForm implements OnInit, AfterViewC
129142
]
130143
],
131144
lock_mode: ['COMPLIANCE'],
132-
lock_retention_period_days: [0, [CdValidators.number(false), lockDaysValidator]]
145+
lock_retention_period_days: [0, [CdValidators.number(false), lockDaysValidator]],
146+
bucket_policy: ['{}', CdValidators.json()]
133147
});
134148
}
135149

@@ -217,6 +231,11 @@ export class RgwBucketFormComponent extends CdForm implements OnInit, AfterViewC
217231
if (value['lock_enabled']) {
218232
this.bucketForm.controls['versioning'].disable();
219233
}
234+
if (value['bucket_policy']) {
235+
this.bucketForm
236+
.get('bucket_policy')
237+
.setValue(JSON.stringify(value['bucket_policy'], null, 2));
238+
}
220239
}
221240
}
222241
this.loadingReady();
@@ -240,6 +259,7 @@ export class RgwBucketFormComponent extends CdForm implements OnInit, AfterViewC
240259
}
241260
const values = this.bucketForm.value;
242261
const xmlStrTags = this.tagsToXML(this.tags);
262+
const bucketPolicy = this.getBucketPolicy();
243263
if (this.editing) {
244264
// Edit
245265
const versioning = this.getVersioningStatus();
@@ -258,7 +278,8 @@ export class RgwBucketFormComponent extends CdForm implements OnInit, AfterViewC
258278
values['mfa-token-pin'],
259279
values['lock_mode'],
260280
values['lock_retention_period_days'],
261-
xmlStrTags
281+
xmlStrTags,
282+
bucketPolicy
262283
)
263284
.subscribe(
264285
() => {
@@ -287,7 +308,8 @@ export class RgwBucketFormComponent extends CdForm implements OnInit, AfterViewC
287308
values['encryption_enabled'],
288309
values['encryption_type'],
289310
values['keyId'],
290-
xmlStrTags
311+
xmlStrTags,
312+
bucketPolicy
291313
)
292314
.subscribe(
293315
() => {
@@ -337,6 +359,10 @@ export class RgwBucketFormComponent extends CdForm implements OnInit, AfterViewC
337359
return this.isMfaDeleteEnabled ? RgwBucketMfaDelete.ENABLED : RgwBucketMfaDelete.DISABLED;
338360
}
339361

362+
getBucketPolicy() {
363+
return this.bucketForm.getValue('bucket_policy') || '{}';
364+
}
365+
340366
fileUpload(files: FileList, controlName: string) {
341367
const file: File = files[0];
342368
const reader = new FileReader();
@@ -349,6 +375,16 @@ export class RgwBucketFormComponent extends CdForm implements OnInit, AfterViewC
349375
});
350376
}
351377

378+
bucketPolicyOnChange() {
379+
if (this.bucketPolicyTextArea) {
380+
this.textAreaJsonFormatterService.format(this.bucketPolicyTextArea);
381+
}
382+
}
383+
384+
openUrl(url: string) {
385+
window.open(url, '_blank');
386+
}
387+
352388
openConfigModal() {
353389
const modalRef = this.modalService.show(RgwConfigModalComponent, null, { size: 'lg' });
354390
modalRef.componentInstance.configForm
@@ -374,6 +410,8 @@ export class RgwBucketFormComponent extends CdForm implements OnInit, AfterViewC
374410

375411
deleteTag(index: number) {
376412
this.tags.splice(index, 1);
413+
this.bucketForm.markAsDirty();
414+
this.bucketForm.updateValueAndValidity();
377415
}
378416

379417
private setTag(tag: Record<string, string>, index?: number) {

src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-bucket.service.spec.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,12 @@ describe('RgwBucketService', () => {
6060
true,
6161
'aws:kms',
6262
'qwerty1',
63+
null,
6364
null
6465
)
6566
.subscribe();
6667
const req = httpTesting.expectOne(
67-
`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}`
68+
`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}`
6869
);
6970
expect(req.request.method).toBe('POST');
7071
});
@@ -84,11 +85,12 @@ describe('RgwBucketService', () => {
8485
'223344',
8586
'GOVERNANCE',
8687
'10',
88+
null,
8789
null
8890
)
8991
.subscribe();
9092
const req = httpTesting.expectOne(
91-
`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`
93+
`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`
9294
);
9395
expect(req.request.method).toBe('PUT');
9496
});

src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-bucket.service.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,8 @@ export class RgwBucketService extends ApiClient {
6060
encryption_state: boolean,
6161
encryption_type: string,
6262
key_id: string,
63-
tags: string
63+
tags: string,
64+
bucketPolicy: string
6465
) {
6566
return this.rgwDaemonService.request((params: HttpParams) => {
6667
return this.http.post(this.url, null, {
@@ -77,6 +78,7 @@ export class RgwBucketService extends ApiClient {
7778
encryption_type,
7879
key_id,
7980
tags: tags,
81+
bucket_policy: bucketPolicy,
8082
daemon_name: params.get('daemon_name')
8183
}
8284
})
@@ -97,7 +99,8 @@ export class RgwBucketService extends ApiClient {
9799
mfaTokenPin: string,
98100
lockMode: 'GOVERNANCE' | 'COMPLIANCE',
99101
lockRetentionPeriodDays: string,
100-
tags: string
102+
tags: string,
103+
bucketPolicy: string
101104
) {
102105
return this.rgwDaemonService.request((params: HttpParams) => {
103106
params = params.appendAll({
@@ -112,7 +115,8 @@ export class RgwBucketService extends ApiClient {
112115
mfa_token_pin: mfaTokenPin,
113116
lock_mode: lockMode,
114117
lock_retention_period_days: lockRetentionPeriodDays,
115-
tags: tags
118+
tags: tags,
119+
bucket_policy: bucketPolicy
116120
});
117121
return this.http.put(`${this.url}/${bucket}`, null, { params: params });
118122
});

src/pybind/mgr/dashboard/frontend/src/app/shared/enum/icons.enum.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ export enum Icons {
8282
navicon = 'fa fa-navicon', // Navigation
8383
areaChart = 'fa fa-area-chart', // Area Chart, dashboard
8484
eye = 'fa fa-eye', // Observability
85+
externalUrl = 'fa fa-external-link', // links to external page
8586

8687
/* Icons for special effect */
8788
large = 'fa fa-lg', // icon becomes 33% larger

src/pybind/mgr/dashboard/frontend/src/app/shared/forms/cd-validators.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -610,4 +610,16 @@ export class CdValidators {
610610
);
611611
};
612612
}
613+
614+
static json(): ValidatorFn {
615+
return (control: AbstractControl): Record<string, any> | null => {
616+
if (!control.value) return null;
617+
try {
618+
JSON.parse(control.value);
619+
return null;
620+
} catch (e) {
621+
return { invalidJson: true };
622+
}
623+
};
624+
}
613625
}
Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Component, ViewChild, ElementRef } from '@angular/core';
22
import { FieldType, FieldTypeConfig } from '@ngx-formly/core';
3+
import { TextAreaJsonFormatterService } from '~/app/shared/services/text-area-json-formatter.service';
34

45
@Component({
56
selector: 'cd-formly-textarea-type',
@@ -10,16 +11,11 @@ export class FormlyTextareaTypeComponent extends FieldType<FieldTypeConfig> {
1011
@ViewChild('textArea')
1112
public textArea: ElementRef<any>;
1213

14+
constructor(private textAreaJsonFormatterService: TextAreaJsonFormatterService) {
15+
super();
16+
}
17+
1318
onChange() {
14-
const value = this.textArea.nativeElement.value;
15-
try {
16-
const formatted = JSON.stringify(JSON.parse(value), null, 2);
17-
this.textArea.nativeElement.value = formatted;
18-
this.textArea.nativeElement.style.height = 'auto';
19-
const lineNumber = formatted.split('\n').length;
20-
const pixelPerLine = 25;
21-
const pixels = lineNumber * pixelPerLine;
22-
this.textArea.nativeElement.style.height = pixels + 'px';
23-
} catch (e) {}
19+
this.textAreaJsonFormatterService.format(this.textArea);
2420
}
2521
}

0 commit comments

Comments
 (0)