Skip to content

Commit e825b4c

Browse files
authored
Merge pull request ceph#64628 from rhcs-dashboard/accounts-enhancements
mgr/dashboard: user accounts enhancements Reviewed-by: Nizamudeen A <[email protected]>
2 parents e13a23c + 6e9db91 commit e825b4c

20 files changed

+345
-89
lines changed

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

Lines changed: 114 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -546,16 +546,51 @@ def get_s3_bucket_name(bucket_name, tenant=None):
546546
bucket_name = '{}:{}'.format(tenant, bucket_name)
547547
return bucket_name
548548

549+
def map_bucket_owners(self, result, daemon_name):
550+
"""
551+
Replace bucket owner IDs with account names for a list of buckets.
552+
553+
:param result: List of bucket dicts with 'owner' keys.
554+
:param daemon_name: RGW daemon identifier.
555+
:return: Modified result with owner names instead of IDs.
556+
"""
557+
# Get unique owner IDs from buckets
558+
owner_ids = {bucket['owner'] for bucket in result}
559+
560+
# Get available account IDs
561+
valid_accounts = set(RgwAccounts().get_accounts())
562+
563+
# Determine which owner IDs are valid and need querying
564+
query_ids = owner_ids & valid_accounts
565+
566+
# Fetch account names for valid owner IDs
567+
id_to_name = {}
568+
for owner_id in query_ids:
569+
try:
570+
account = self.proxy(daemon_name, 'GET', 'account', {'id': owner_id})
571+
if 'name' in account:
572+
id_to_name[owner_id] = account['name']
573+
except RequestException:
574+
continue
575+
576+
# Replace owner IDs with names in the bucket list
577+
for bucket in result:
578+
owner_id = bucket.get('owner')
579+
if owner_id in id_to_name:
580+
bucket['owner'] = id_to_name[owner_id]
581+
582+
return result
583+
549584
@RESTController.MethodMap(version=APIVersion(1, 1)) # type: ignore
550585
def list(self, stats: bool = False, daemon_name: Optional[str] = None,
551586
uid: Optional[str] = None) -> List[Union[str, Dict[str, Any]]]:
552587
query_params = f'?stats={str_to_bool(stats)}'
553588
if uid and uid.strip():
554589
query_params = f'{query_params}&uid={uid.strip()}'
555590
result = self.proxy(daemon_name, 'GET', 'bucket{}'.format(query_params))
556-
557-
if stats:
591+
if str_to_bool(stats):
558592
result = [self._append_bid(bucket) for bucket in result]
593+
result = self.map_bucket_owners(result, daemon_name)
559594

560595
return result
561596

@@ -900,6 +935,9 @@ def _get(self, uid, daemon_name=None, stats=True) -> dict:
900935
if not self._keys_allowed():
901936
del result['keys']
902937
del result['swift_keys']
938+
if result.get('account_id') not in (None, '') and result.get('type') != 'root':
939+
rgwAccounts = RgwAccounts()
940+
result['managed_user_policies'] = rgwAccounts.list_managed_policy(uid)
903941
result['uid'] = result['full_user_id']
904942
return result
905943

@@ -918,53 +956,94 @@ def get_emails(self, daemon_name=None):
918956
def create(self, uid, display_name, email=None, max_buckets=None,
919957
system=None, suspended=None, generate_key=None, access_key=None,
920958
secret_key=None, daemon_name=None, account_id: Optional[str] = None,
921-
account_root_user: Optional[bool] = False):
922-
params = {'uid': uid}
923-
if display_name is not None:
924-
params['display-name'] = display_name
925-
if email is not None:
926-
params['email'] = email
927-
if max_buckets is not None:
928-
params['max-buckets'] = max_buckets
929-
if system is not None:
930-
params['system'] = system
931-
if suspended is not None:
932-
params['suspended'] = suspended
933-
if generate_key is not None:
934-
params['generate-key'] = generate_key
935-
if access_key is not None:
936-
params['access-key'] = access_key
937-
if secret_key is not None:
938-
params['secret-key'] = secret_key
939-
if account_id is not None:
940-
params['account-id'] = account_id
959+
account_root_user: Optional[bool] = False,
960+
account_policies: Optional[str] = None):
961+
"""Create a new RGW user."""
962+
963+
params = {'uid': uid, 'display-name': display_name}
964+
965+
# Add optional parameters
966+
optional_params = {
967+
'email': email,
968+
'max-buckets': max_buckets,
969+
'system': system,
970+
'suspended': suspended,
971+
'generate-key': generate_key,
972+
'access-key': access_key,
973+
'secret-key': secret_key,
974+
'account-id': account_id
975+
}
976+
977+
# Add only non-None parameters
978+
for key, value in optional_params.items():
979+
if value is not None:
980+
params[key] = value
981+
982+
# Handle boolean parameter separately
941983
if account_root_user:
942984
params['account-root'] = account_root_user
985+
986+
# Make the API request
943987
result = self.proxy(daemon_name, 'PUT', 'user', params)
944988
result['uid'] = result['full_user_id']
989+
990+
# Process account policies
991+
if account_policies is not None:
992+
self._process_account_policies(uid, account_policies)
993+
945994
return result
946995

996+
def _process_account_policies(self, uid, account_policies):
997+
"""Process account policies for a user."""
998+
rgw_accounts = RgwAccounts()
999+
# Parse the policies JSON if it's a string
1000+
if isinstance(account_policies, str):
1001+
account_policies = json.loads(account_policies)
1002+
1003+
# Attach policies
1004+
for policy_arn in account_policies.get('attach', []):
1005+
rgw_accounts.attach_managed_policy(uid, policy_arn)
1006+
1007+
# Detach policies
1008+
for policy_arn in account_policies.get('detach', []):
1009+
rgw_accounts.detach_managed_policy(uid, policy_arn)
1010+
9471011
@allow_empty_body
9481012
def set(self, uid, display_name=None, email=None, max_buckets=None,
9491013
system=None, suspended=None, daemon_name=None, account_id: Optional[str] = None,
950-
account_root_user: Optional[bool] = False):
1014+
account_root_user: Optional[bool] = False,
1015+
account_policies: Optional[str] = None):
1016+
"""Update an existing RGW user."""
1017+
9511018
params = {'uid': uid}
952-
if display_name is not None:
953-
params['display-name'] = display_name
954-
if email is not None:
955-
params['email'] = email
956-
if max_buckets is not None:
957-
params['max-buckets'] = max_buckets
958-
if system is not None:
959-
params['system'] = system
960-
if suspended is not None:
961-
params['suspended'] = suspended
962-
if account_id is not None:
963-
params['account-id'] = account_id
1019+
1020+
# Add optional parameters
1021+
optional_params = {
1022+
'display-name': display_name,
1023+
'email': email,
1024+
'max-buckets': max_buckets,
1025+
'system': system,
1026+
'suspended': suspended,
1027+
'account-id': account_id
1028+
}
1029+
1030+
# Add only non-None parameters
1031+
for key, value in optional_params.items():
1032+
if value is not None:
1033+
params[key] = value
1034+
1035+
# Handle boolean parameter separately
9641036
if account_root_user:
9651037
params['account-root'] = account_root_user
1038+
1039+
# Make the API request
9661040
result = self.proxy(daemon_name, 'POST', 'user', params)
9671041
result['uid'] = result['full_user_id']
1042+
1043+
# Process account policies
1044+
if account_policies is not None:
1045+
self._process_account_policies(uid, account_policies)
1046+
9681047
return result
9691048

9701049
def delete(self, uid, daemon_name=None):

src/pybind/mgr/dashboard/frontend/package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/pybind/mgr/dashboard/frontend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
"@angular/platform-browser-dynamic": "18.2.11",
5353
"@angular/router": "18.2.11",
5454
"@carbon/charts-angular": "1.23.9",
55-
"@carbon/icons": "11.41.0",
55+
"@carbon/icons": "11.63.0",
5656
"@carbon/styles": "1.83.0",
5757
"@ibm/plex": "6.4.0",
5858
"@ng-bootstrap/ng-bootstrap": "17.0.1",

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

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,12 @@ export class RgwBucketFormComponent extends CdForm implements OnInit, AfterViewC
231231
}
232232

233233
// Process route parameters.
234-
this.route.params.subscribe((params: { bid: string }) => {
234+
this.route.params.subscribe((params: { bid: string; owner: string }) => {
235+
let bucketOwner = '';
236+
if (params.hasOwnProperty('owner')) {
237+
// only used for showing bucket owned by account
238+
bucketOwner = decodeURIComponent(params.owner);
239+
}
235240
if (params.hasOwnProperty('bid')) {
236241
const bid = decodeURIComponent(params.bid);
237242
promises['getBid'] = this.rgwBucketService.get(bid);
@@ -299,13 +304,13 @@ export class RgwBucketFormComponent extends CdForm implements OnInit, AfterViewC
299304
// creating dummy user object to show the account owner
300305
// here value['owner] is the account user id
301306
const user = Object.assign(
302-
{ uid: value['owner'] },
307+
{ uid: bucketOwner },
303308
ownersList.find((owner: RgwUser) => owner.uid === AppConstants.defaultUser)
304309
);
305310
this.accountUsers.push(user);
306311
this.bucketForm.get('isAccountOwner').setValue(true);
307312
this.bucketForm.get('isAccountOwner').disable();
308-
this.bucketForm.get('accountUser').setValue(value['owner']);
313+
this.bucketForm.get('accountUser').setValue(bucketOwner);
309314
this.bucketForm.get('accountUser').disable();
310315
}
311316
this.isVersioningAlreadyEnabled = this.isVersioningEnabled;

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,10 @@ export class RgwBucketListComponent extends ListWithDetails implements OnInit, O
110110
}
111111
];
112112
const getBucketUri = () =>
113-
this.selection.first() && `${encodeURIComponent(this.selection.first().bid)}`;
113+
this.selection.first() &&
114+
`${encodeURIComponent(this.selection.first().bid)}/${encodeURIComponent(
115+
this.selection.first().owner
116+
)}`;
114117
const addAction: CdTableAction = {
115118
permission: 'create',
116119
icon: Icons.add,

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,13 @@
6464
class="bold">Maximum buckets</td>
6565
<td>{{ user.max_buckets | map: maxBucketsMap }}</td>
6666
</tr>
67+
@if (user.type === 'rgw' && selection.account?.id){
68+
<tr>
69+
<td i18n
70+
class="bold">Managed policies</td>
71+
<td i18n>{{ extractPolicyNamesFromArns(user.managed_user_policies) }}</td>
72+
</tr>
73+
}
6774
<tr *ngIf="user.subusers && user.subusers.length">
6875
<td i18n
6976
class="bold">Subusers</td>

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,9 @@ describe('RgwUserDetailsComponent', () => {
5656
system: 'true',
5757
keys: [],
5858
swift_keys: [],
59-
mfa_ids: ['testMFA1', 'testMFA2']
59+
mfa_ids: ['testMFA1', 'testMFA2'],
60+
type: 'rgw',
61+
account: { id: 'RGW12345678901234567' }
6062
};
6163

6264
component.ngOnChanges();
@@ -65,8 +67,8 @@ describe('RgwUserDetailsComponent', () => {
6567
const detailsTab = fixture.debugElement.nativeElement.querySelectorAll(
6668
'.cds--data-table--sort.cds--data-table--no-border tr td'
6769
);
68-
expect(detailsTab[14].textContent).toEqual('MFAs(Id)');
69-
expect(detailsTab[15].textContent).toEqual('testMFA1, testMFA2');
70+
expect(detailsTab[16].textContent).toEqual('MFAs(Id)');
71+
expect(detailsTab[17].textContent).toEqual('testMFA1, testMFA2');
7072
});
7173
it('should test updateKeysSelection', () => {
7274
component.selection = {

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,4 +136,14 @@ export class RgwUserDetailsComponent implements OnChanges, OnInit {
136136
break;
137137
}
138138
}
139+
140+
extractPolicyNamesFromArns(arnList: string[]) {
141+
if (!arnList || arnList.length === 0) {
142+
return '-';
143+
}
144+
return arnList
145+
.map((arn) => arn.trim().split('/').pop())
146+
.filter(Boolean)
147+
.join(', ');
148+
}
139149
}

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

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
[ngValue]="null">Loading...</option>
2424
<option i18n
2525
*ngIf="accounts !== null"
26-
[ngValue]="null">-- Select an Account --</option>
26+
[value]="''">-- Select an Account --</option>
2727
<option *ngFor="let account of accounts"
2828
[value]="account.id">{{ account.name }} {{account.tenant ? '- '+account.tenant : ''}}</option>
2929
</cds-select>
@@ -208,6 +208,31 @@
208208
</cds-checkbox>
209209
</div>
210210

211+
@if(userForm.getValue('account_id') && !userForm.getValue('account_root_user')) {
212+
<!-- Managed policies -->
213+
<fieldset>
214+
<div class="form-item">
215+
<legend i18n
216+
class="cd-header">Managed policies</legend>
217+
<cds-combo-box label="Policies"
218+
type="multi"
219+
selectionFeedback="top-after-reopen"
220+
formControlName="account_policies"
221+
id="account_policies"
222+
placeholder="Select managed policies..."
223+
i18n-placeholder
224+
[appendInline]="true"
225+
[items]="managedPolicies"
226+
(selected)="multiSelector($event)"
227+
itemValueKey="name"
228+
i18n-label
229+
i18n>
230+
<cds-dropdown-list></cds-dropdown-list>
231+
</cds-combo-box>
232+
</div>
233+
</fieldset>
234+
}
235+
211236
<!-- S3 key -->
212237
<fieldset *ngIf="!editing">
213238
<legend i18n

0 commit comments

Comments
 (0)