bucket = '/{}'.format(bucket)
# Link bucket to new user:
+ params = {
+ 'bucket': bucket,
+ 'bucket-id': bucket_id,
+ }
+
+ accounts = RgwAccounts().get_accounts()
+ if uid in accounts:
+ # If the bucket is owned by an account, we need to use the account-id
+ # instead of uid.
+ params['account-id'] = uid
+ else:
+ params['uid'] = uid
result = self.proxy(daemon_name,
'PUT',
- 'bucket', {
- 'bucket': bucket,
- 'bucket-id': bucket_id,
- 'uid': uid
- },
+ 'bucket', params,
json_response=False)
uid_tenant = uid[:uid.find('$')] if uid.find('$') >= 0 else None
and len(set(edit_permissions).intersection(set(permissions[Scope.RGW]))) > 0
@EndpointDoc("Display RGW Users",
+ parameters={
+ 'detailed': (bool, "If true, returns complete user details for each user. "
+ "If false, returns only the list of usernames.")
+ },
responses={200: RGW_USER_SCHEMA})
- def list(self, daemon_name=None):
- # type: (Optional[str]) -> List[str]
- users = [] # type: List[str]
+ def list(self, daemon_name=None, detailed: bool = False):
+ detailed = str_to_bool(detailed)
+ users = [] # type: List[Union[str, Dict[str, Any]]]
marker = None
while True:
params = {} # type: dict
if marker:
params['marker'] = marker
result = self.proxy(daemon_name, 'GET', 'user?list', params)
+ if detailed:
+ for user in result['keys']:
+ users.append(self._get(user, daemon_name=daemon_name, stats=False))
+ return users
users.extend(result['keys'])
if not result['truncated']:
break
</ng-template>
</div>
- <!-- Owner -->
- <div class="form-item">
- <cds-select label="Owner"
- for="owner"
- formControlName="owner"
- name="owner"
- id="owner"
- [invalidText]="ownerError"
- [invalid]="!bucketForm.controls.owner.valid && bucketForm.controls.owner.dirty"
- cdRequiredField="Owner"
- i18n>Owner
- <option *ngIf="owners === null"
- [ngValue]="null">Loading...</option>
- <option *ngIf="owners !== null"
- value="">-- Select a user --</option>
- <option *ngFor="let owner of owners"
- [value]="owner">{{ owner }}</option>
- </cds-select>
- <ng-template #ownerError>
- <span class="invalid-feedback"
- *ngIf="bucketForm.showError('owner', frm, 'required')"
- i18n>This field is required.</span>
- </ng-template>
- <cd-alert-panel
- type="info"
- *ngIf="bucketForm.get('owner').disabled"
- spacingClass="me-1 mt-1"
- i18n>
- The bucket is owned by an account. UI does not support changing
- the ownership of bucket owned by an account.
- </cd-alert-panel>
- </div>
+ <!-- Accounts -->
+ @if(accounts.length && accountUsers.length > 0){
+ <cds-checkbox formControlName="isAccountOwner"
+ i18n>Select account user
+ </cds-checkbox>
+ }
+
+ @if (bucketForm.get('isAccountOwner').value) {
+ <!-- Account user -->
+ <div class="form-item">
+ <cds-select label="Account user"
+ id="acc_user"
+ formControlName="accountUser"
+ [invalidText]="accountUserError"
+ [invalid]="!bucketForm.controls.accountUser.valid && bucketForm.controls.accountUser.dirty"
+ cdRequiredField="Account user"
+ i18n>Account user
+ <option *ngIf="accountusers === null"
+ [ngValue]="null">Loading...</option>
+ <option *ngIf="accountusers !== null"
+ value="">-- Select a user --</option>
+ <option *ngFor="let user of accountUsers"
+ [value]="user.uid">{{ user.uid }}</option>
+ </cds-select>
+ <ng-template #accountUserError>
+ <span class="invalid-feedback"
+ *ngIf="bucketForm.showError('accountUser', frm, 'required')"
+ i18n>This field is required.</span>
+ </ng-template>
+ <cd-alert-panel
+ type="info"
+ *ngIf="bucketForm.get('accountUser').disabled"
+ i18n>
+ The bucket is owned by an account. UI does not support changing
+ the ownership of bucket owned by an account.
+ </cd-alert-panel>
+ </div>
+ } @else {
+ <!-- Owner -->
+ <div class="form-item">
+ <cds-select label="Owner"
+ formControlName="owner"
+ id="owner"
+ [invalidText]="ownerError"
+ [invalid]="!bucketForm.controls.owner.valid && bucketForm.controls.owner.dirty"
+ cdRequiredField="Owner"
+ i18n>Owner
+ <option *ngIf="owners === null"
+ [ngValue]="null">Loading...</option>
+ <option *ngIf="owners !== null"
+ value="">-- Select a user --</option>
+ <option *ngFor="let owner of owners"
+ [value]="owner.uid">{{ owner.uid }}</option>
+ </cds-select>
+ <ng-template #ownerError>
+ <span class="invalid-feedback"
+ *ngIf="bucketForm.showError('owner', frm, 'required')"
+ i18n>This field is required.</span>
+ </ng-template>
+ </div>
+ }
<!-- Versioning -->
<fieldset *ngIf="editing">
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { By } from '@angular/platform-browser';
import { LoadingStatus } from '~/app/shared/forms/cd-form';
+import { RgwUserAccountsService } from '~/app/shared/api/rgw-user-accounts.service';
describe('RgwBucketFormComponent', () => {
let component: RgwBucketFormComponent;
let getPlacementTargetsSpy: jasmine.Spy;
let rgwBucketServiceGetSpy: jasmine.Spy;
let enumerateSpy: jasmine.Spy;
+ let accountListSpy: jasmine.Spy;
let formHelper: FormHelper;
let childComponent: RgwRateLimitComponent;
rgwBucketServiceGetSpy = spyOn(rgwBucketService, 'get');
getPlacementTargetsSpy = spyOn(TestBed.inject(RgwSiteService), 'get');
enumerateSpy = spyOn(TestBed.inject(RgwUserService), 'enumerate');
+ accountListSpy = spyOn(TestBed.inject(RgwUserAccountsService), 'list');
formHelper = new FormHelper(component.bucketForm);
});
};
getPlacementTargetsSpy.and.returnValue(observableOf(payload));
enumerateSpy.and.returnValue(observableOf([]));
+ accountListSpy.and.returnValue(observableOf([]));
fixture.detectChanges();
expect(component.zonegroup).toBe(payload.zonegroup);
import { RgwBucketService } from '~/app/shared/api/rgw-bucket.service';
import { RgwSiteService } from '~/app/shared/api/rgw-site.service';
import { RgwUserService } from '~/app/shared/api/rgw-user.service';
-import { ActionLabelsI18n, URLVerbs } from '~/app/shared/constants/app.constants';
+import { ActionLabelsI18n, AppConstants, URLVerbs } from '~/app/shared/constants/app.constants';
import { Icons } from '~/app/shared/enum/icons.enum';
import { NotificationType } from '~/app/shared/enum/notification-type.enum';
import { CdForm } from '~/app/shared/forms/cd-form';
import { RgwRateLimitComponent } from '../rgw-rate-limit/rgw-rate-limit.component';
import { RgwRateLimitConfig } from '../models/rgw-rate-limit';
import { ModalCdsService } from '~/app/shared/services/modal-cds.service';
+import { RgwUserAccountsService } from '~/app/shared/api/rgw-user-accounts.service';
+import { RgwUser } from '../models/rgw-user';
@Component({
selector: 'cd-rgw-bucket-form',
bucketForm: CdFormGroup;
editing = false;
- owners: string[] = null;
+ owners: RgwUser[] = null;
+ accounts: string[] = [];
+ accountUsers: RgwUser[] = [];
kmsProviders: string[] = null;
action: string;
resource: string;
public actionLabels: ActionLabelsI18n,
private readonly changeDetectorRef: ChangeDetectorRef,
private rgwMultisiteService: RgwMultisiteService,
- private rgwDaemonService: RgwDaemonService
+ private rgwDaemonService: RgwDaemonService,
+ private rgwAccountsService: RgwUserAccountsService
) {
super();
this.editing = this.router.url.startsWith(`/rgw/bucket/${URLVerbs.EDIT}`);
? []
: [CdValidators.bucketName(), CdValidators.bucketExistence(false, this.rgwBucketService)]
],
- owner: [null, [Validators.required]],
+ owner: [
+ null,
+ [
+ CdValidators.requiredIf({
+ isAccountOwner: false
+ })
+ ]
+ ],
kms_provider: ['vault'],
'placement-target': [null],
versioning: [null],
lifecycle: ['{}', CdValidators.jsonOrXml()],
grantee: [Grantee.Owner, [Validators.required]],
aclPermission: [[aclPermission.FullControl], [Validators.required]],
- replication: [false]
+ replication: [false],
+ isAccountOwner: [false],
+ accountUser: [
+ null,
+ [
+ CdValidators.requiredIf({
+ isAccountOwner: true
+ })
+ ]
+ ]
});
}
ngOnInit() {
const promises = {
- owners: this.rgwUserService.enumerate()
+ owners: this.rgwUserService.enumerate(true),
+ accounts: this.rgwAccountsService.list()
};
this.multisiteStatus$ = this.rgwMultisiteService.status();
this.isDefaultZoneGroup$ = this.rgwDaemonService.selectedDaemon$.pipe(
forkJoin(promises).subscribe((data: any) => {
// Get the list of possible owners.
- this.owners = (<string[]>data.owners).sort();
-
+ this.accounts = data.accounts;
+ this.accountUsers = data.owners.filter((owner: RgwUser) => owner.account_id);
+ this.owners = data.owners.filter((owner: RgwUser) => !owner.account_id);
// Get placement targets:
if (data['getPlacementTargets']) {
const placementTargets = data['getPlacementTargets'];
}
this.bucketForm.setValue(value);
if (this.editing) {
- // temporary fix until the s3 account management is implemented in
- // the frontend. Disable changing the owner of the bucket in case
+ // Disable changing the owner of the bucket in case
// its owned by the account.
- // @TODO: Introduce account selection for a bucket.
- if (!this.owners.includes(value['owner'])) {
- this.owners.push(value['owner']);
- this.bucketForm.get('owner').disable();
+ const ownersList = data.owners.map((owner: RgwUser) => owner.uid);
+ if (!ownersList.includes(value['owner'])) {
+ // creating dummy user object to show the account owner
+ // here value['owner] is the account user id
+ const user = Object.assign(
+ { uid: value['owner'] },
+ ownersList.find((owner: RgwUser) => owner.uid === AppConstants.defaultUser)
+ );
+ this.accountUsers.push(user);
+ this.bucketForm.get('isAccountOwner').setValue(true);
+ this.bucketForm.get('isAccountOwner').disable();
+ this.bucketForm.get('accountUser').setValue(value['owner']);
+ this.bucketForm.get('accountUser').disable();
}
this.isVersioningAlreadyEnabled = this.isVersioningEnabled;
this.isMfaDeleteAlreadyEnabled = this.isMfaDeleteEnabled;
// make the owner empty if the field is disabled.
// this ensures the bucket doesn't gets updated with owner when
// the bucket is owned by the account.
- const owner = this.bucketForm.get('owner').disabled === true ? '' : values['owner'];
+ let owner;
+ if (this.bucketForm.get('accountUser').disabled) {
+ // If the bucket is owned by the account, then set the owner as account user.
+ owner = '';
+ } else if (values['isAccountOwner']) {
+ const accountUser: RgwUser = this.accountUsers.filter(
+ (user) => values['accountUser'] === user.uid
+ )[0];
+ owner = accountUser?.account_id ?? values['owner'];
+ } else {
+ owner = values['owner'];
+ }
+
this.rgwBucketService
.update(
values['bid'],
}
);
} else {
+ const owner = values['isAccountOwner'] ? values['accountUser'] : values['owner'];
// Add
this.rgwBucketService
.create(
values['bid'],
- values['owner'],
+ owner,
this.zonegroup,
values['placement-target'],
values['lock_enabled'],
* Scenario 2: Changing the bucket owner from non-tenanted to tenanted
* Scenario 3: Keeping the owner(tenanted) same and changing only rate limit
*/
- const owner = this.bucketForm.getValue('owner');
+ // in case of creating bucket with account user, owner will be empty
+ const owner = this.bucketForm.getValue('owner') || '';
const bidInput = this.bucketForm.getValue('bid');
let bid: string;
service.list().subscribe((resp) => {
result = resp;
});
- const req = httpTesting.expectOne(`api/rgw/user?${RgwHelper.DAEMON_QUERY_PARAM}`);
+ const req = httpTesting.expectOne(
+ `api/rgw/user?${RgwHelper.DAEMON_QUERY_PARAM}&detailed=false`
+ );
expect(req.request.method).toBe('GET');
req.flush([]);
expect(result).toEqual([]);
service.list().subscribe((resp) => {
result = resp;
});
- let req = httpTesting.expectOne(`api/rgw/user?${RgwHelper.DAEMON_QUERY_PARAM}`);
+ let req = httpTesting.expectOne(`api/rgw/user?${RgwHelper.DAEMON_QUERY_PARAM}&detailed=false`);
expect(req.request.method).toBe('GET');
req.flush(['foo', 'bar']);
it('should call enumerate', () => {
service.enumerate().subscribe();
- const req = httpTesting.expectOne(`api/rgw/user?${RgwHelper.DAEMON_QUERY_PARAM}`);
+ const req = httpTesting.expectOne(
+ `api/rgw/user?${RgwHelper.DAEMON_QUERY_PARAM}&detailed=false`
+ );
expect(req.request.method).toBe('GET');
});
* Get the list of usernames.
* @return {Observable<string[]>}
*/
- enumerate() {
+ enumerate(detailed: boolean = false) {
return this.rgwDaemonService.request((params: HttpParams) => {
- return this.http.get(this.url, { params: params });
+ params = params.append('detailed', detailed);
+ return this.http.get(this.url, { params });
});
}
export class AppConstants {
public static readonly organization = 'ceph';
public static readonly projectName = 'Ceph Dashboard';
+ public static readonly defaultUser = 'dashboard';
public static readonly license = 'Free software (LGPL 2.1).';
public static readonly copyright = 'Copyright(c) ' + environment.year + ' Ceph contributors.';
public static readonly cephLogo = 'assets/Ceph_Logo.svg';
name: daemon_name
schema:
type: string
+ - default: false
+ description: If true, returns complete user details for each user. If false,
+ returns only the list of usernames.
+ in: query
+ name: detailed
+ schema:
+ type: boolean
responses:
'200':
content:
destination = ET.SubElement(rule, 'Destination')
bucket = ET.SubElement(destination, 'Bucket')
- bucket.text = bucket_name
+ bucket.text = 'arn:aws:s3:::'f'{bucket_name}'
replication_config = ET.tostring(root, encoding='utf-8', method='xml').decode()