Skip to content

Commit 0d4f2e3

Browse files
author
Naman Munet
committed
mgr/dashboard: link user to rgw account & add root account user functionality
Fixes: https://tracker.ceph.com/issues/69529 Signed-off-by: Naman Munet <[email protected]>
1 parent 316f14b commit 0d4f2e3

File tree

9 files changed

+386
-28
lines changed

9 files changed

+386
-28
lines changed

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

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -851,7 +851,8 @@ def get_emails(self, daemon_name=None):
851851
@allow_empty_body
852852
def create(self, uid, display_name, email=None, max_buckets=None,
853853
system=None, suspended=None, generate_key=None, access_key=None,
854-
secret_key=None, daemon_name=None):
854+
secret_key=None, daemon_name=None, account_id: Optional[str] = None,
855+
account_root_user: Optional[bool] = False):
855856
params = {'uid': uid}
856857
if display_name is not None:
857858
params['display-name'] = display_name
@@ -869,13 +870,18 @@ def create(self, uid, display_name, email=None, max_buckets=None,
869870
params['access-key'] = access_key
870871
if secret_key is not None:
871872
params['secret-key'] = secret_key
873+
if account_id is not None:
874+
params['account-id'] = account_id
875+
if account_root_user:
876+
params['account-root'] = account_root_user
872877
result = self.proxy(daemon_name, 'PUT', 'user', params)
873878
result['uid'] = result['full_user_id']
874879
return result
875880

876881
@allow_empty_body
877882
def set(self, uid, display_name=None, email=None, max_buckets=None,
878-
system=None, suspended=None, daemon_name=None):
883+
system=None, suspended=None, daemon_name=None, account_id: Optional[str] = None,
884+
account_root_user: Optional[bool] = False):
879885
params = {'uid': uid}
880886
if display_name is not None:
881887
params['display-name'] = display_name
@@ -887,6 +893,10 @@ def set(self, uid, display_name=None, email=None, max_buckets=None,
887893
params['system'] = system
888894
if suspended is not None:
889895
params['suspended'] = suspended
896+
if account_id is not None:
897+
params['account-id'] = account_id
898+
if account_root_user:
899+
params['account-root'] = account_root_user
890900
result = self.proxy(daemon_name, 'POST', 'user', params)
891901
result['uid'] = result['full_user_id']
892902
return result
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
interface Key {
2+
access_key: string;
3+
active: boolean;
4+
secret_key: string;
5+
user: string;
6+
}
7+
8+
interface SwiftKey {
9+
active: boolean;
10+
secret_key: string;
11+
user: string;
12+
}
13+
14+
interface Cap {
15+
perm: string;
16+
type: string;
17+
}
18+
19+
interface Subuser {
20+
id: string;
21+
permissions: string;
22+
}
23+
24+
interface BucketQuota {
25+
check_on_raw: boolean;
26+
enabled: boolean;
27+
max_objects: number;
28+
max_size: number;
29+
max_size_kb: number;
30+
}
31+
32+
interface UserQuota {
33+
check_on_raw: boolean;
34+
enabled: boolean;
35+
max_objects: number;
36+
max_size: number;
37+
max_size_kb: number;
38+
}
39+
40+
interface Stats {
41+
num_objects: number;
42+
size: number;
43+
size_actual: number;
44+
size_utilized: number;
45+
size_kb: number;
46+
size_kb_actual: number;
47+
size_kb_utilized: number;
48+
}
49+
50+
export interface RgwUser {
51+
account_id: string;
52+
admin: boolean;
53+
bucket_quota: BucketQuota;
54+
caps: Cap[];
55+
create_date: string;
56+
default_placement: string;
57+
default_storage_class: string;
58+
display_name: string;
59+
email: string;
60+
full_user_id: string;
61+
group_ids: any[];
62+
keys: Key[];
63+
max_buckets: number;
64+
mfa_ids: any[];
65+
op_mask: string;
66+
path: string;
67+
placement_tags: any[];
68+
stats: Stats;
69+
subusers: Subuser[];
70+
suspended: number;
71+
swift_keys: SwiftKey[];
72+
system: boolean;
73+
tags: any[];
74+
tenant: string;
75+
temp_url_keys: any[];
76+
type: string;
77+
uid: string;
78+
user_id: string;
79+
user_quota: UserQuota;
80+
}

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

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,35 @@
8888
</tbody>
8989
</table>
9090

91+
<ng-container *ngIf="selection.account && selection.account?.id">
92+
<legend i18n>Account Details</legend>
93+
<table class="cds--data-table--sort cds--data-table--no-border cds--data-table cds--data-table--md">
94+
<tbody>
95+
<tr>
96+
<td i18n
97+
class="bold w-25">Account ID</td>
98+
<td class="w-75">{{ selection.account?.id }}</td>
99+
</tr>
100+
<tr>
101+
<td i18n
102+
class="bold w-25">Name</td>
103+
<td class="w-75">{{ selection.account?.name }}</td>
104+
</tr>
105+
<tr>
106+
<td i18n
107+
class="bold w-25">Tenant</td>
108+
<td class="w-75">{{ selection.account?.tenant || '-'}}</td>
109+
</tr>
110+
<tr>
111+
<td i18n
112+
class="bold w-25">User type</td>
113+
<td class="w-75"
114+
i18n>{{ user?.type === 'root' ? 'Account root user' : 'rgw user' }}</td>
115+
</tr>
116+
</tbody>
117+
</table>
118+
</ng-container>
119+
91120
<!-- User quota -->
92121
<div *ngIf="user.user_quota">
93122
<legend i18n>User quota</legend>

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

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,49 @@
77
<div i18n="form title"
88
class="form-header">{{ action | titlecase }} {{ resource | upperFirst }}</div>
99

10+
@if(accounts.length > 0){
11+
<!-- Link Account -->
12+
<div class="form-item">
13+
<cds-select label="Link Account"
14+
i18n-label
15+
for="link_account"
16+
formControlName="account_id"
17+
[invalid]="userForm.controls.account_id.invalid && userForm.controls.account_id.dirty"
18+
[invalidText]="accountError"
19+
[helperText]="accountsHelper">
20+
<option i18n
21+
*ngIf="accounts === null"
22+
[ngValue]="null">Loading...</option>
23+
<option i18n
24+
*ngIf="accounts !== null"
25+
[ngValue]="null">-- Select an Account --</option>
26+
<option *ngFor="let account of accounts"
27+
[value]="account.id">{{ account.name }} {{account.tenant ? '- '+account.tenant : ''}}</option>
28+
</cds-select>
29+
<ng-template #accountError>
30+
<span class="invalid-feedback"
31+
*ngIf="userForm.showError('account_id', frm, 'tenantedAccount')"
32+
i18n>Only accounts with the same tenant name can be linked to a tenanted user.</span>
33+
</ng-template>
34+
<ng-template #accountsHelper>
35+
<div i18n>Account membership is permanent. Once added, users cannot be removed from their account.</div>
36+
<div i18n>Ownership of all of the user's buckets will be transferred to the account.</div>
37+
</ng-template>
38+
</div>
39+
40+
<!-- Account Root user -->
41+
<div *ngIf="userForm.getValue('account_id')"
42+
class="form-item">
43+
<cds-checkbox formControlName="account_root_user"
44+
id="account_root_user"
45+
i18n>Account Root user
46+
<cd-help-text>The account root user has full access to all resources and manages the account.
47+
It's recommended to use this account for management tasks only and create additional users with specific permissions.
48+
</cd-help-text>
49+
</cds-checkbox>
50+
</div>
51+
}
52+
1053
<!-- User ID -->
1154
<div class="form-item">
1255
<cds-text-label for="user_id"
@@ -35,14 +78,14 @@
3578
</ng-template>
3679
</div>
3780

38-
<!-- Show Tenant -->
39-
<div class="form-item">
40-
<cds-checkbox formControlName="show_tenant"
41-
id="show_tenant"
42-
[readonly]="true"
43-
(checkedChange)="updateFieldsWhenTenanted()">Show Tenant
44-
</cds-checkbox>
45-
</div>
81+
<!-- Show Tenant -->
82+
<div class="form-item">
83+
<cds-checkbox formControlName="show_tenant"
84+
id="show_tenant"
85+
[readonly]="true"
86+
(checkedChange)="updateFieldsWhenTenanted()">Show Tenant
87+
</cds-checkbox>
88+
</div>
4689

4790
<!-- Tenant -->
4891
<div class="form-item"

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

Lines changed: 69 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { FormatterService } from '~/app/shared/services/formatter.service';
2323
import { RgwRateLimitComponent } from '../rgw-rate-limit/rgw-rate-limit.component';
2424
import { By } from '@angular/platform-browser';
2525
import { CheckboxModule, NumberModule, SelectModule } from 'carbon-components-angular';
26+
import { LoadingStatus } from '~/app/shared/forms/cd-form';
2627

2728
describe('RgwUserFormComponent', () => {
2829
let component: RgwUserFormComponent;
@@ -185,6 +186,7 @@ describe('RgwUserFormComponent', () => {
185186

186187
describe('max buckets', () => {
187188
beforeEach(() => {
189+
component.loading = LoadingStatus.Ready;
188190
fixture.detectChanges();
189191
childComponent = fixture.debugElement.query(By.directive(RgwRateLimitComponent))
190192
.componentInstance;
@@ -203,7 +205,9 @@ describe('RgwUserFormComponent', () => {
203205
secret_key: '',
204206
suspended: false,
205207
system: false,
206-
uid: null
208+
uid: null,
209+
account_id: '',
210+
account_root_user: false
207211
});
208212
expect(spyRateLimit).toHaveBeenCalled();
209213
});
@@ -219,7 +223,8 @@ describe('RgwUserFormComponent', () => {
219223
email: null,
220224
max_buckets: -1,
221225
suspended: false,
222-
system: false
226+
system: false,
227+
account_root_user: false
223228
});
224229
expect(spyRateLimit).toHaveBeenCalled();
225230
});
@@ -238,7 +243,9 @@ describe('RgwUserFormComponent', () => {
238243
secret_key: '',
239244
suspended: false,
240245
system: false,
241-
uid: null
246+
uid: null,
247+
account_id: '',
248+
account_root_user: false
242249
});
243250
expect(spyRateLimit).toHaveBeenCalled();
244251
});
@@ -254,7 +261,8 @@ describe('RgwUserFormComponent', () => {
254261
email: null,
255262
max_buckets: 0,
256263
suspended: false,
257-
system: false
264+
system: false,
265+
account_root_user: false
258266
});
259267
expect(spyRateLimit).toHaveBeenCalled();
260268
});
@@ -264,6 +272,7 @@ describe('RgwUserFormComponent', () => {
264272
formHelper.setValue('max_buckets_mode', 1, true);
265273
formHelper.setValue('max_buckets', 100, true);
266274
let spyRateLimit = jest.spyOn(childComponent, 'getRateLimitFormValue');
275+
267276
component.onSubmit();
268277
expect(rgwUserService.create).toHaveBeenCalledWith({
269278
access_key: '',
@@ -274,7 +283,9 @@ describe('RgwUserFormComponent', () => {
274283
secret_key: '',
275284
suspended: false,
276285
system: false,
277-
uid: null
286+
uid: null,
287+
account_id: '',
288+
account_root_user: false
278289
});
279290
expect(spyRateLimit).toHaveBeenCalled();
280291
});
@@ -291,7 +302,8 @@ describe('RgwUserFormComponent', () => {
291302
email: null,
292303
max_buckets: 100,
293304
suspended: false,
294-
system: false
305+
system: false,
306+
account_root_user: false
295307
});
296308
expect(spyRateLimit).toHaveBeenCalled();
297309
});
@@ -301,6 +313,7 @@ describe('RgwUserFormComponent', () => {
301313
let notificationService: NotificationService;
302314

303315
beforeEach(() => {
316+
component.loading = LoadingStatus.Ready;
304317
spyOn(TestBed.inject(Router), 'navigate').and.stub();
305318
notificationService = TestBed.inject(NotificationService);
306319
spyOn(notificationService, 'show');
@@ -320,7 +333,8 @@ describe('RgwUserFormComponent', () => {
320333
email: '',
321334
max_buckets: 1000,
322335
suspended: false,
323-
system: false
336+
system: false,
337+
account_root_user: false
324338
});
325339
});
326340

@@ -348,6 +362,9 @@ describe('RgwUserFormComponent', () => {
348362
});
349363

350364
describe('RgwUserCapabilities', () => {
365+
beforeEach(() => {
366+
component.loading = LoadingStatus.Ready;
367+
});
351368
it('capability button disabled when all capabilities are added', () => {
352369
component.editing = true;
353370
for (const capabilityType of RgwUserCapabilities.getAll()) {
@@ -669,4 +686,49 @@ describe('RgwUserFormComponent', () => {
669686
expect(modalRef.submitAction.subscribe).toHaveBeenCalled();
670687
});
671688
});
689+
690+
describe('RgwUserAccounts', () => {
691+
beforeEach(() => {
692+
component.loading = LoadingStatus.Ready;
693+
fixture.detectChanges();
694+
childComponent = fixture.debugElement.query(By.directive(RgwRateLimitComponent))
695+
.componentInstance;
696+
});
697+
it('create with account id & account root user', () => {
698+
spyOn(rgwUserService, 'create');
699+
formHelper.setValue('account_id', 'RGW12312312312312312', true);
700+
formHelper.setValue('account_root_user', true, true);
701+
component.onSubmit();
702+
expect(rgwUserService.create).toHaveBeenCalledWith({
703+
access_key: '',
704+
display_name: null,
705+
email: '',
706+
generate_key: true,
707+
max_buckets: 1000,
708+
secret_key: '',
709+
suspended: false,
710+
system: false,
711+
uid: null,
712+
account_id: 'RGW12312312312312312',
713+
account_root_user: true
714+
});
715+
});
716+
717+
it('edit to link account to existing user', () => {
718+
spyOn(rgwUserService, 'update');
719+
component.editing = true;
720+
formHelper.setValue('account_id', 'RGW12312312312312312', true);
721+
formHelper.setValue('account_root_user', true, true);
722+
component.onSubmit();
723+
expect(rgwUserService.update).toHaveBeenCalledWith(null, {
724+
display_name: null,
725+
email: null,
726+
max_buckets: 1000,
727+
suspended: false,
728+
system: false,
729+
account_id: 'RGW12312312312312312',
730+
account_root_user: true
731+
});
732+
});
733+
});
672734
});

0 commit comments

Comments
 (0)