bucket_name = '{}:{}'.format(tenant, bucket_name)
return bucket_name
+ def map_bucket_owners(self, result, daemon_name):
+ """
+ Replace bucket owner IDs with account names for a list of buckets.
+
+ :param result: List of bucket dicts with 'owner' keys.
+ :param daemon_name: RGW daemon identifier.
+ :return: Modified result with owner names instead of IDs.
+ """
+ # Get unique owner IDs from buckets
+ owner_ids = {bucket['owner'] for bucket in result}
+
+ # Get available account IDs
+ valid_accounts = set(RgwAccounts().get_accounts())
+
+ # Determine which owner IDs are valid and need querying
+ query_ids = owner_ids & valid_accounts
+
+ # Fetch account names for valid owner IDs
+ id_to_name = {}
+ for owner_id in query_ids:
+ try:
+ account = self.proxy(daemon_name, 'GET', 'account', {'id': owner_id})
+ if 'name' in account:
+ id_to_name[owner_id] = account['name']
+ except RequestException:
+ continue
+
+ # Replace owner IDs with names in the bucket list
+ for bucket in result:
+ owner_id = bucket.get('owner')
+ if owner_id in id_to_name:
+ bucket['owner'] = id_to_name[owner_id]
+
+ return result
+
@RESTController.MethodMap(version=APIVersion(1, 1)) # type: ignore
def list(self, stats: bool = False, daemon_name: Optional[str] = None,
uid: Optional[str] = None) -> List[Union[str, Dict[str, Any]]]:
if uid and uid.strip():
query_params = f'{query_params}&uid={uid.strip()}'
result = self.proxy(daemon_name, 'GET', 'bucket{}'.format(query_params))
-
- if stats:
+ if str_to_bool(stats):
result = [self._append_bid(bucket) for bucket in result]
+ result = self.map_bucket_owners(result, daemon_name)
return result
if not self._keys_allowed():
del result['keys']
del result['swift_keys']
+ if result.get('account_id') not in (None, '') and result.get('type') != 'root':
+ rgwAccounts = RgwAccounts()
+ result['managed_user_policies'] = rgwAccounts.list_managed_policy(uid)
result['uid'] = result['full_user_id']
return result
def create(self, uid, display_name, email=None, max_buckets=None,
system=None, suspended=None, generate_key=None, access_key=None,
secret_key=None, daemon_name=None, account_id: Optional[str] = None,
- account_root_user: Optional[bool] = False):
- params = {'uid': uid}
- if display_name is not None:
- params['display-name'] = display_name
- if email is not None:
- params['email'] = email
- if max_buckets is not None:
- params['max-buckets'] = max_buckets
- if system is not None:
- params['system'] = system
- if suspended is not None:
- params['suspended'] = suspended
- if generate_key is not None:
- params['generate-key'] = generate_key
- if access_key is not None:
- params['access-key'] = access_key
- if secret_key is not None:
- params['secret-key'] = secret_key
- if account_id is not None:
- params['account-id'] = account_id
+ account_root_user: Optional[bool] = False,
+ account_policies: Optional[str] = None):
+ """Create a new RGW user."""
+
+ params = {'uid': uid, 'display-name': display_name}
+
+ # Add optional parameters
+ optional_params = {
+ 'email': email,
+ 'max-buckets': max_buckets,
+ 'system': system,
+ 'suspended': suspended,
+ 'generate-key': generate_key,
+ 'access-key': access_key,
+ 'secret-key': secret_key,
+ 'account-id': account_id
+ }
+
+ # Add only non-None parameters
+ for key, value in optional_params.items():
+ if value is not None:
+ params[key] = value
+
+ # Handle boolean parameter separately
if account_root_user:
params['account-root'] = account_root_user
+
+ # Make the API request
result = self.proxy(daemon_name, 'PUT', 'user', params)
result['uid'] = result['full_user_id']
+
+ # Process account policies
+ if account_policies is not None:
+ self._process_account_policies(uid, account_policies)
+
return result
+ def _process_account_policies(self, uid, account_policies):
+ """Process account policies for a user."""
+ rgw_accounts = RgwAccounts()
+ # Parse the policies JSON if it's a string
+ if isinstance(account_policies, str):
+ account_policies = json.loads(account_policies)
+
+ # Attach policies
+ for policy_arn in account_policies.get('attach', []):
+ rgw_accounts.attach_managed_policy(uid, policy_arn)
+
+ # Detach policies
+ for policy_arn in account_policies.get('detach', []):
+ rgw_accounts.detach_managed_policy(uid, policy_arn)
+
@allow_empty_body
def set(self, uid, display_name=None, email=None, max_buckets=None,
system=None, suspended=None, daemon_name=None, account_id: Optional[str] = None,
- account_root_user: Optional[bool] = False):
+ account_root_user: Optional[bool] = False,
+ account_policies: Optional[str] = None):
+ """Update an existing RGW user."""
+
params = {'uid': uid}
- if display_name is not None:
- params['display-name'] = display_name
- if email is not None:
- params['email'] = email
- if max_buckets is not None:
- params['max-buckets'] = max_buckets
- if system is not None:
- params['system'] = system
- if suspended is not None:
- params['suspended'] = suspended
- if account_id is not None:
- params['account-id'] = account_id
+
+ # Add optional parameters
+ optional_params = {
+ 'display-name': display_name,
+ 'email': email,
+ 'max-buckets': max_buckets,
+ 'system': system,
+ 'suspended': suspended,
+ 'account-id': account_id
+ }
+
+ # Add only non-None parameters
+ for key, value in optional_params.items():
+ if value is not None:
+ params[key] = value
+
+ # Handle boolean parameter separately
if account_root_user:
params['account-root'] = account_root_user
+
+ # Make the API request
result = self.proxy(daemon_name, 'POST', 'user', params)
result['uid'] = result['full_user_id']
+
+ # Process account policies
+ if account_policies is not None:
+ self._process_account_policies(uid, account_policies)
+
return result
def delete(self, uid, daemon_name=None):
"@angular/platform-browser-dynamic": "18.2.11",
"@angular/router": "18.2.11",
"@carbon/charts-angular": "1.23.9",
- "@carbon/icons": "11.41.0",
+ "@carbon/icons": "11.63.0",
"@carbon/styles": "1.83.0",
"@ibm/plex": "6.4.0",
"@ng-bootstrap/ng-bootstrap": "17.0.1",
"license": "Apache-2.0"
},
"node_modules/@carbon/icons": {
- "version": "11.41.0",
- "resolved": "https://registry.npmjs.org/@carbon/icons/-/icons-11.41.0.tgz",
- "integrity": "sha512-9RGaOnihPQx74yBQ0UnEr9JJ+e2aa/J+tmTG/sZ203q2hfoeMF2PqipwOhNS1fqCnyW1zvsYQNydUsNIDzCqaA==",
+ "version": "11.63.0",
+ "resolved": "https://registry.npmjs.org/@carbon/icons/-/icons-11.63.0.tgz",
+ "integrity": "sha512-J5sGbamMMBbQPcdX9ImzEIoa7l2DyNiZYu9ScKtY3dJ6lKeG6GJUFQYH5/vcpuyj02tizVe6rSgWcH210+OOqw==",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"@angular/platform-browser-dynamic": "18.2.11",
"@angular/router": "18.2.11",
"@carbon/charts-angular": "1.23.9",
- "@carbon/icons": "11.41.0",
+ "@carbon/icons": "11.63.0",
"@carbon/styles": "1.83.0",
"@ibm/plex": "6.4.0",
"@ng-bootstrap/ng-bootstrap": "17.0.1",
}
// Process route parameters.
- this.route.params.subscribe((params: { bid: string }) => {
+ this.route.params.subscribe((params: { bid: string; owner: string }) => {
+ let bucketOwner = '';
+ if (params.hasOwnProperty('owner')) {
+ // only used for showing bucket owned by account
+ bucketOwner = decodeURIComponent(params.owner);
+ }
if (params.hasOwnProperty('bid')) {
const bid = decodeURIComponent(params.bid);
promises['getBid'] = this.rgwBucketService.get(bid);
// creating dummy user object to show the account owner
// here value['owner] is the account user id
const user = Object.assign(
- { uid: value['owner'] },
+ { uid: bucketOwner },
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').setValue(bucketOwner);
this.bucketForm.get('accountUser').disable();
}
this.isVersioningAlreadyEnabled = this.isVersioningEnabled;
}
];
const getBucketUri = () =>
- this.selection.first() && `${encodeURIComponent(this.selection.first().bid)}`;
+ this.selection.first() &&
+ `${encodeURIComponent(this.selection.first().bid)}/${encodeURIComponent(
+ this.selection.first().owner
+ )}`;
const addAction: CdTableAction = {
permission: 'create',
icon: Icons.add,
class="bold">Maximum buckets</td>
<td>{{ user.max_buckets | map: maxBucketsMap }}</td>
</tr>
+ @if (user.type === 'rgw' && selection.account?.id){
+ <tr>
+ <td i18n
+ class="bold">Managed policies</td>
+ <td i18n>{{ extractPolicyNamesFromArns(user.managed_user_policies) }}</td>
+ </tr>
+ }
<tr *ngIf="user.subusers && user.subusers.length">
<td i18n
class="bold">Subusers</td>
system: 'true',
keys: [],
swift_keys: [],
- mfa_ids: ['testMFA1', 'testMFA2']
+ mfa_ids: ['testMFA1', 'testMFA2'],
+ type: 'rgw',
+ account: { id: 'RGW12345678901234567' }
};
component.ngOnChanges();
const detailsTab = fixture.debugElement.nativeElement.querySelectorAll(
'.cds--data-table--sort.cds--data-table--no-border tr td'
);
- expect(detailsTab[14].textContent).toEqual('MFAs(Id)');
- expect(detailsTab[15].textContent).toEqual('testMFA1, testMFA2');
+ expect(detailsTab[16].textContent).toEqual('MFAs(Id)');
+ expect(detailsTab[17].textContent).toEqual('testMFA1, testMFA2');
});
it('should test updateKeysSelection', () => {
component.selection = {
break;
}
}
+
+ extractPolicyNamesFromArns(arnList: string[]) {
+ if (!arnList || arnList.length === 0) {
+ return '-';
+ }
+ return arnList
+ .map((arn) => arn.trim().split('/').pop())
+ .filter(Boolean)
+ .join(', ');
+ }
}
[ngValue]="null">Loading...</option>
<option i18n
*ngIf="accounts !== null"
- [ngValue]="null">-- Select an Account --</option>
+ [value]="''">-- Select an Account --</option>
<option *ngFor="let account of accounts"
[value]="account.id">{{ account.name }} {{account.tenant ? '- '+account.tenant : ''}}</option>
</cds-select>
</cds-checkbox>
</div>
+ @if(userForm.getValue('account_id') && !userForm.getValue('account_root_user')) {
+ <!-- Managed policies -->
+ <fieldset>
+ <div class="form-item">
+ <legend i18n
+ class="cd-header">Managed policies</legend>
+ <cds-combo-box label="Policies"
+ type="multi"
+ selectionFeedback="top-after-reopen"
+ formControlName="account_policies"
+ id="account_policies"
+ placeholder="Select managed policies..."
+ i18n-placeholder
+ [appendInline]="true"
+ [items]="managedPolicies"
+ (selected)="multiSelector($event)"
+ itemValueKey="name"
+ i18n-label
+ i18n>
+ <cds-dropdown-list></cds-dropdown-list>
+ </cds-combo-box>
+ </div>
+ </fieldset>
+ }
+
<!-- S3 key -->
<fieldset *ngIf="!editing">
<legend i18n
secret_key: '',
suspended: false,
system: false,
- uid: null,
- account_id: '',
- account_root_user: false
+ uid: null
});
expect(spyRateLimit).toHaveBeenCalled();
});
email: null,
max_buckets: -1,
suspended: false,
- system: false,
- account_root_user: false
+ system: false
});
expect(spyRateLimit).toHaveBeenCalled();
});
secret_key: '',
suspended: false,
system: false,
- uid: null,
- account_id: '',
- account_root_user: false
+ uid: null
});
expect(spyRateLimit).toHaveBeenCalled();
});
email: null,
max_buckets: 0,
suspended: false,
- system: false,
- account_root_user: false
+ system: false
});
expect(spyRateLimit).toHaveBeenCalled();
});
secret_key: '',
suspended: false,
system: false,
- uid: null,
- account_id: '',
- account_root_user: false
+ uid: null
});
expect(spyRateLimit).toHaveBeenCalled();
});
email: null,
max_buckets: 100,
suspended: false,
- system: false,
- account_root_user: false
+ system: false
});
expect(spyRateLimit).toHaveBeenCalled();
});
email: '',
max_buckets: 1000,
suspended: false,
- system: false,
- account_root_user: false
+ system: false
});
});
import { ModalCdsService } from '~/app/shared/services/modal-cds.service';
import { RgwUserAccountsService } from '~/app/shared/api/rgw-user-accounts.service';
import { Account } from '../models/rgw-user-accounts';
-import { RGW } from '../utils/constants';
+import { ManagedPolicyArnMap, ManagedPolicyName, RGW } from '../utils/constants';
+import { ComboBoxItem } from '~/app/shared/models/combo-box.model';
@Component({
selector: 'cd-rgw-user-form',
previousTenant: string = null;
@ViewChild(RgwRateLimitComponent, { static: false }) rateLimitComponent!: RgwRateLimitComponent;
accounts: Account[] = [];
+ initialUserPolicies: string[] = [];
+ managedPolicies: ComboBoxItem[] = [
+ {
+ content: ManagedPolicyName.AmazonS3FullAccess,
+ name: ManagedPolicyArnMap[ManagedPolicyName.AmazonS3FullAccess],
+ selected: false
+ },
+ {
+ content: ManagedPolicyName.AmazonS3ReadOnlyAccess,
+ name: ManagedPolicyArnMap[ManagedPolicyName.AmazonS3ReadOnlyAccess],
+ selected: false
+ }
+ ];
constructor(
private formBuilder: CdFormBuilder,
],
account_id: [null, [this.tenantedAccountValidator.bind(this)]],
account_root_user: [false],
+ account_policies: [[]],
max_buckets_mode: [1],
max_buckets: [
1000,
});
this.capabilities = resp[0].caps;
this.uid = this.getUID();
+ this.initialUserPolicies = resp[0].managed_user_policies ?? [];
+
+ this.managedPolicies.forEach((policy) => {
+ policy.selected = this.initialUserPolicies.includes(policy.name);
+ });
+
+ // Optionally, update form control with selected items
+ const selectedItems = this.managedPolicies.filter((p) => p.selected).map((p) => p.name);
+ this.userForm.get('account_policies')?.setValue(selectedItems);
},
() => {
this.loadingError();
});
}
+ multiSelector(event: ComboBoxItem[]) {
+ this.managedPolicies.forEach((policy) => {
+ policy.selected = !!event.find((selected) => selected.name === policy.name);
+ });
+ }
+
tenantedAccountValidator(control: AbstractControl): ValidationErrors | null {
if (this?.userForm?.getValue('tenant') && this.accounts.length > 0) {
const index: number = this.accounts.findIndex(
'system',
'suspended',
'account_id',
- 'account_root_user'
+ 'account_root_user',
+ 'account_policies'
].some((path) => {
return this.userForm.get(path).dirty;
});
private _getCreateArgs() {
const result = {
uid: this.getUID(),
- account_id: this.userForm.getValue('account_id') ? this.userForm.getValue('account_id') : '',
- account_root_user: this.userForm.getValue('account_root_user'),
display_name: this.userForm.getValue('display_name'),
system: this.userForm.getValue('system'),
suspended: this.userForm.getValue('suspended'),
// 0 => Unlimited bucket creation.
_.merge(result, { max_buckets: maxBucketsMode });
}
+ if (this.userForm.getValue('account_id')) {
+ _.merge(result, {
+ account_id: this.userForm.getValue('account_id'),
+ account_root_user: this.userForm.getValue('account_root_user')
+ });
+ }
+ const accountPolicies = this._getAccountManagedPolicies();
+ if (this.userForm.getValue('account_id') && !this.userForm.getValue('account_root_user')) {
+ _.merge(result, { account_policies: accountPolicies });
+ }
return result;
}
*/
private _getUpdateArgs() {
const result: Record<string, any> = {};
- const keys = [
- 'display_name',
- 'email',
- 'max_buckets',
- 'system',
- 'suspended',
- 'account_root_user'
- ];
+ const keys = ['display_name', 'email', 'max_buckets', 'system', 'suspended'];
for (const key of keys) {
result[key] = this.userForm.getValue(key);
}
if (this.userForm.getValue('account_id')) {
result['account_id'] = this.userForm.getValue('account_id');
+ result['account_root_user'] = this.userForm.getValue('account_root_user');
}
const maxBucketsMode = parseInt(this.userForm.getValue('max_buckets_mode'), 10);
if (_.includes([-1, 0], maxBucketsMode)) {
// 0 => Unlimited bucket creation.
result['max_buckets'] = maxBucketsMode;
}
+ const accountPolicies = this._getAccountManagedPolicies();
+ if (this.userForm.getValue('account_id') && !this.userForm.getValue('account_root_user')) {
+ result['account_policies'] = accountPolicies;
+ }
return result;
}
return result;
}
+ /**
+ * Get the account managed policies to attach/detach.
+ * @returns {Object} Returns an object with attach and detach arrays.
+ */
+ private _getAccountManagedPolicies() {
+ const selectedPolicies = this.managedPolicies.filter((p) => p.selected).map((p) => p.name);
+
+ const initialPolicies = this.initialUserPolicies;
+ const toAttach = selectedPolicies.filter((p) => !initialPolicies.includes(p));
+ const toDetach = initialPolicies.filter((p) => !selectedPolicies.includes(p));
+
+ const payload = {
+ attach: toAttach,
+ detach: toDetach
+ };
+
+ return payload;
+ }
+
onMaxBucketsModeChange(mode: string) {
if (mode === '1') {
// If 'Custom' mode is selected, then ensure that the form field
i18n>No Limit</ng-template>
</ng-template>
-<ng-template #accountTmpl
+<ng-template #usernameTpl
let-row="data.row">
- <cds-tooltip [description]="row.account?.name ? (row.type === 'root' ? 'Account root user' :'') : ''"
- [align]="'top'"
- i18n-description
- i18n>{{row.account?.name}}</cds-tooltip>
+ <div cdsRow>
+ <span i18n>{{ row.uid }}</span>
+ @if (row.type === 'root') {
+ <cds-tooltip [description]="'Account root user'"
+ [align]="'top'"
+ i18n-description>
+ <svg [cdsIcon]="icons.userAccessLocked"
+ [size]="icons.size16"
+ class="account-root-icon"></svg>
+ </cds-tooltip>
+ }
+ </div>
</ng-template>
+@use '@carbon/layout';
+
+.account-root-icon {
+ margin-left: layout.$spacing-03;
+}
userSizeTpl: TemplateRef<any>;
@ViewChild('userObjectTpl', { static: true })
userObjectTpl: TemplateRef<any>;
- @ViewChild('accountTmpl', { static: true })
- public accountTmpl: TemplateRef<any>;
+ @ViewChild('usernameTpl', { static: true })
+ usernameTpl: TemplateRef<any>;
permission: Permission;
tableActions: CdTableAction[];
columns: CdTableColumn[] = [];
selection: CdTableSelection = new CdTableSelection();
userDataSubject = new Subject();
declare staleTimeout: number;
+ icons = Icons;
constructor(
private authStorageService: AuthStorageService,
{
name: $localize`Username`,
prop: 'uid',
- flexGrow: 1
+ flexGrow: 1,
+ cellTemplate: this.usernameTpl
},
{
name: $localize`Tenant`,
{
name: $localize`Account name`,
prop: 'account.name',
- flexGrow: 1,
- cellTemplate: this.accountTmpl
+ flexGrow: 1
},
{
name: $localize`Full name`,
import ProgressBarRoundIcon from '@carbon/icons/es/progress-bar--round/32';
import ToolsIcon from '@carbon/icons/es/tools/32';
import ParentChild from '@carbon/icons/es/parent-child/20';
+import UserAccessLocked from '@carbon/icons/es/user--access-locked/16';
import { CephSharedModule } from '../shared/ceph-shared.module';
import { RgwUserAccountsComponent } from './rgw-user-accounts/rgw-user-accounts.component';
ArrowDownIcon,
ProgressBarRoundIcon,
ToolsIcon,
- ParentChild
+ ParentChild,
+ UserAccessLocked
]);
}
}
data: { breadcrumbs: ActionLabels.CREATE }
},
{
- path: `${URLVerbs.EDIT}/:bid`,
+ path: `${URLVerbs.EDIT}/:bid/:owner`,
component: RgwBucketFormComponent,
data: { breadcrumbs: ActionLabels.EDIT }
}
export const RGW = 'rgw';
+
+export enum ManagedPolicyName {
+ AmazonS3FullAccess = 'AmazonS3FullAccess',
+ AmazonS3ReadOnlyAccess = 'AmazonS3ReadOnlyAccess'
+}
+
+export const ManagedPolicyArnMap: Record<ManagedPolicyName, string> = {
+ [ManagedPolicyName.AmazonS3FullAccess]: 'arn:aws:iam::aws:policy/AmazonS3FullAccess',
+ [ManagedPolicyName.AmazonS3ReadOnlyAccess]: 'arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess'
+};
create(args: Record<string, any>) {
return this.rgwDaemonService.request((params: HttpParams) => {
_.keys(args).forEach((key) => {
+ if (typeof args[key] === 'object') {
+ args[key] = JSON.stringify(args[key]);
+ }
params = params.append(key, args[key]);
});
return this.http.post(this.url, null, { params: params });
update(uid: string, args: Record<string, any>) {
return this.rgwDaemonService.request((params: HttpParams) => {
_.keys(args).forEach((key) => {
+ if (typeof args[key] === 'object') {
+ args[key] = JSON.stringify(args[key]);
+ }
params = params.append(key, args[key]);
});
return this.http.put(`${this.url}/${uid}`, null, { params: params });
parentChild = 'parent-child',
dataTable = 'data-table',
idea = 'idea',
+ userAccessLocked = 'user--access-locked', // User access locked
/* Icons for special effect */
size16 = '16',
size20 = '20',
tags:
- RgwUser
post:
+ description: Create a new RGW user.
parameters: []
requestBody:
content:
type: string
account_id:
type: integer
+ account_policies:
+ type: integer
account_root_user:
default: false
type: integer
tags:
- RgwUser
put:
+ description: Update an existing RGW user.
parameters:
- in: path
name: uid
properties:
account_id:
type: integer
+ account_policies:
+ type: integer
account_root_user:
default: false
type: integer
'--account-id', account_id]
return cls.send_rgw_cmd(set_quota_status_cmd)
+
+ @classmethod
+ def attach_managed_policy(cls, userId, policy_arn):
+ radosgw_attach_managed_policies = ['user', 'policy', 'attach',
+ '--uid', userId, '--policy-arn', policy_arn]
+ try:
+ exit_code, _, err = mgr.send_rgwadmin_command(radosgw_attach_managed_policies,
+ stdout_as_json=False)
+ if exit_code > 0:
+ raise DashboardException(e=err, msg='Unable to attach managed policies',
+ http_status_code=500, component='rgw')
+ except SubprocessError as error:
+ raise DashboardException(error, http_status_code=500, component='rgw')
+
+ @classmethod
+ def detach_managed_policy(cls, userId, policy_arn):
+ radosgw_detach_managed_policy = ['user', 'policy', 'detach',
+ '--uid', userId, '--policy-arn', policy_arn]
+ try:
+ exit_code, _, err = mgr.send_rgwadmin_command(radosgw_detach_managed_policy,
+ stdout_as_json=False)
+ if exit_code > 0:
+ raise DashboardException(e=err, msg='Unable to detach managed policies',
+ http_status_code=500, component='rgw')
+
+ except SubprocessError as error:
+ raise DashboardException(error, http_status_code=500, component='rgw')
+
+ @classmethod
+ def list_managed_policy(cls, userId):
+ radosgw_list_managed_policies = ['user', 'policy', 'list', 'attached',
+ '--uid', userId]
+ try:
+ exit_code, out, err = mgr.send_rgwadmin_command(radosgw_list_managed_policies)
+ if exit_code > 0:
+ raise DashboardException(e=err, msg='Unable to get managed policies',
+ http_status_code=500, component='rgw')
+ return out
+ except SubprocessError as error:
+ raise DashboardException(error, http_status_code=500, component='rgw')