1 import { Component, OnInit } from '@angular/core';
2 import { AbstractControl, ValidationErrors, Validators } from '@angular/forms';
3 import { ActivatedRoute, Router } from '@angular/router';
5 import { I18n } from '@ngx-translate/i18n-polyfill';
6 import * as _ from 'lodash';
7 import { BsModalService } from 'ngx-bootstrap/modal';
8 import { concat as observableConcat, forkJoin as observableForkJoin, Observable } from 'rxjs';
10 import { RgwUserService } from '../../../shared/api/rgw-user.service';
11 import { ActionLabelsI18n, URLVerbs } from '../../../shared/constants/app.constants';
12 import { Icons } from '../../../shared/enum/icons.enum';
13 import { NotificationType } from '../../../shared/enum/notification-type.enum';
14 import { CdFormBuilder } from '../../../shared/forms/cd-form-builder';
15 import { CdFormGroup } from '../../../shared/forms/cd-form-group';
16 import { CdValidators, isEmptyInputValue } from '../../../shared/forms/cd-validators';
17 import { FormatterService } from '../../../shared/services/formatter.service';
18 import { NotificationService } from '../../../shared/services/notification.service';
19 import { RgwUserCapabilities } from '../models/rgw-user-capabilities';
20 import { RgwUserCapability } from '../models/rgw-user-capability';
21 import { RgwUserS3Key } from '../models/rgw-user-s3-key';
22 import { RgwUserSubuser } from '../models/rgw-user-subuser';
23 import { RgwUserSwiftKey } from '../models/rgw-user-swift-key';
24 import { RgwUserCapabilityModalComponent } from '../rgw-user-capability-modal/rgw-user-capability-modal.component';
25 import { RgwUserS3KeyModalComponent } from '../rgw-user-s3-key-modal/rgw-user-s3-key-modal.component';
26 import { RgwUserSubuserModalComponent } from '../rgw-user-subuser-modal/rgw-user-subuser-modal.component';
27 import { RgwUserSwiftKeyModalComponent } from '../rgw-user-swift-key-modal/rgw-user-swift-key-modal.component';
30 selector: 'cd-rgw-user-form',
31 templateUrl: './rgw-user-form.component.html',
32 styleUrls: ['./rgw-user-form.component.scss']
34 export class RgwUserFormComponent implements OnInit {
35 userForm: CdFormGroup;
39 submitObservables: Observable<Object>[] = [];
41 subusers: RgwUserSubuser[] = [];
42 s3Keys: RgwUserS3Key[] = [];
43 swiftKeys: RgwUserSwiftKey[] = [];
44 capabilities: RgwUserCapability[] = [];
50 capabilityLabel: string;
53 private formBuilder: CdFormBuilder,
54 private route: ActivatedRoute,
55 private router: Router,
56 private rgwUserService: RgwUserService,
57 private bsModalService: BsModalService,
58 private notificationService: NotificationService,
60 public actionLabels: ActionLabelsI18n
62 this.resource = this.i18n('user');
63 this.subuserLabel = this.i18n('subuser');
64 this.s3keyLabel = this.i18n('S3 Key');
65 this.capabilityLabel = this.i18n('capability');
70 this.userForm = this.formBuilder.group({
74 [Validators.required],
75 [CdValidators.unique(this.rgwUserService.exists, this.rgwUserService)]
77 display_name: [null, [Validators.required]],
81 [CdValidators.unique(this.rgwUserService.emailExists, this.rgwUserService)]
83 max_buckets_mode: [1],
87 CdValidators.requiredIf({ max_buckets_mode: '1' }),
88 CdValidators.number(false),
95 access_key: [null, [CdValidators.requiredIf({ generate_key: false })]],
96 secret_key: [null, [CdValidators.requiredIf({ generate_key: false })]],
98 user_quota_enabled: [false],
99 user_quota_max_size_unlimited: [true],
100 user_quota_max_size: [
103 CdValidators.composeIf(
105 user_quota_enabled: true,
106 user_quota_max_size_unlimited: false
108 [Validators.required, this.quotaMaxSizeValidator]
112 user_quota_max_objects_unlimited: [true],
113 user_quota_max_objects: [
117 CdValidators.requiredIf({
118 user_quota_enabled: true,
119 user_quota_max_objects_unlimited: false
124 bucket_quota_enabled: [false],
125 bucket_quota_max_size_unlimited: [true],
126 bucket_quota_max_size: [
129 CdValidators.composeIf(
131 bucket_quota_enabled: true,
132 bucket_quota_max_size_unlimited: false
134 [Validators.required, this.quotaMaxSizeValidator]
138 bucket_quota_max_objects_unlimited: [true],
139 bucket_quota_max_objects: [
143 CdValidators.requiredIf({
144 bucket_quota_enabled: true,
145 bucket_quota_max_objects_unlimited: false
153 this.editing = this.router.url.startsWith(`/rgw/user/${URLVerbs.EDIT}`);
154 this.action = this.editing ? this.actionLabels.EDIT : this.actionLabels.CREATE;
155 // Process route parameters.
156 this.route.params.subscribe((params: { uid: string }) => {
157 if (!params.hasOwnProperty('uid')) {
160 const uid = decodeURIComponent(params.uid);
162 // Load the user and quota information.
163 const observables = [];
164 observables.push(this.rgwUserService.get(uid));
165 observables.push(this.rgwUserService.getQuota(uid));
166 observableForkJoin(observables).subscribe(
168 this.loading = false;
169 // Get the default values.
170 const defaults = _.clone(this.userForm.value);
171 // Extract the values displayed in the form.
172 let value = _.pick(resp[0], _.keys(this.userForm.value));
173 // Map the max. buckets values.
174 switch (value['max_buckets']) {
176 value['max_buckets_mode'] = -1;
177 value['max_buckets'] = '';
180 value['max_buckets_mode'] = 0;
181 value['max_buckets'] = '';
184 value['max_buckets_mode'] = 1;
187 // Map the quota values.
188 ['user', 'bucket'].forEach((type) => {
189 const quota = resp[1][type + '_quota'];
190 value[type + '_quota_enabled'] = quota.enabled;
191 if (quota.max_size < 0) {
192 value[type + '_quota_max_size_unlimited'] = true;
193 value[type + '_quota_max_size'] = null;
195 value[type + '_quota_max_size_unlimited'] = false;
196 value[type + '_quota_max_size'] = `${quota.max_size} B`;
198 if (quota.max_objects < 0) {
199 value[type + '_quota_max_objects_unlimited'] = true;
200 value[type + '_quota_max_objects'] = null;
202 value[type + '_quota_max_objects_unlimited'] = false;
203 value[type + '_quota_max_objects'] = quota.max_objects;
206 // Merge with default values.
207 value = _.merge(defaults, value);
209 this.userForm.setValue(value);
211 // Get the sub users.
212 this.subusers = resp[0].subusers;
215 this.s3Keys = resp[0].keys;
216 this.swiftKeys = resp[0].swift_keys;
218 // Process the capabilities.
219 const mapPerm = { 'read, write': '*' };
220 resp[0].caps.forEach((cap: any) => {
221 if (cap.perm in mapPerm) {
222 cap.perm = mapPerm[cap.perm];
225 this.capabilities = resp[0].caps;
235 this.router.navigate(['/rgw/user']);
239 let notificationTitle: string;
240 // Exit immediately if the form isn't dirty.
241 if (this.userForm.pristine) {
245 const uid = this.userForm.getValue('uid');
248 if (this._isGeneralDirty()) {
249 const args = this._getUpdateArgs();
250 this.submitObservables.push(this.rgwUserService.update(uid, args));
252 notificationTitle = this.i18n('Updated Object Gateway user "{{uid}}"', { uid: uid });
255 const args = this._getCreateArgs();
256 this.submitObservables.push(this.rgwUserService.create(args));
257 notificationTitle = this.i18n('Created Object Gateway user "{{uid}}"', { uid: uid });
259 // Check if user quota has been modified.
260 if (this._isUserQuotaDirty()) {
261 const userQuotaArgs = this._getUserQuotaArgs();
262 this.submitObservables.push(this.rgwUserService.updateQuota(uid, userQuotaArgs));
264 // Check if bucket quota has been modified.
265 if (this._isBucketQuotaDirty()) {
266 const bucketQuotaArgs = this._getBucketQuotaArgs();
267 this.submitObservables.push(this.rgwUserService.updateQuota(uid, bucketQuotaArgs));
269 // Finally execute all observables one by one in serial.
270 observableConcat(...this.submitObservables).subscribe({
272 // Reset the 'Submit' button.
273 this.userForm.setErrors({ cdSubmitButton: true });
276 this.notificationService.show(NotificationType.success, notificationTitle);
283 * Validate the quota maximum size, e.g. 1096, 1K, 30M or 1.9MiB.
285 quotaMaxSizeValidator(control: AbstractControl): ValidationErrors | null {
286 if (isEmptyInputValue(control.value)) {
289 const m = RegExp('^(\\d+(\\.\\d+)?)\\s*(B|K(B|iB)?|M(B|iB)?|G(B|iB)?|T(B|iB)?)?$', 'i').exec(
293 return { quotaMaxSize: true };
295 const bytes = new FormatterService().toBytes(control.value);
296 return bytes < 1024 ? { quotaMaxSize: true } : null;
300 * Add/Update a subuser.
302 setSubuser(subuser: RgwUserSubuser, index?: number) {
303 const mapPermissions: Record<string, string> = {
304 'full-control': 'full',
305 'read-write': 'readwrite'
307 const uid = this.userForm.getValue('uid');
311 subuser.permissions in mapPermissions
312 ? mapPermissions[subuser.permissions]
313 : subuser.permissions,
315 secret_key: subuser.secret_key,
316 generate_secret: subuser.generate_secret ? 'true' : 'false'
318 this.submitObservables.push(this.rgwUserService.createSubuser(uid, args));
319 if (_.isNumber(index)) {
321 // Create an observable to modify the subuser when the form is submitted.
322 this.subusers[index] = subuser;
325 // Create an observable to add the subuser when the form is submitted.
326 this.subusers.push(subuser);
327 // Add a Swift key. If the secret key is auto-generated, then visualize
328 // this to the user by displaying a notification instead of the key.
329 this.swiftKeys.push({
331 secret_key: subuser.generate_secret ? 'Apply your changes first...' : subuser.secret_key
334 // Mark the form as dirty to be able to submit it.
335 this.userForm.markAsDirty();
340 * @param {number} index The subuser to delete.
342 deleteSubuser(index: number) {
343 const subuser = this.subusers[index];
344 // Create an observable to delete the subuser when the form is submitted.
345 this.submitObservables.push(
346 this.rgwUserService.deleteSubuser(this.userForm.getValue('uid'), subuser.id)
348 // Remove the associated S3 keys.
349 this.s3Keys = this.s3Keys.filter((key) => {
350 return key.user !== subuser.id;
352 // Remove the associated Swift keys.
353 this.swiftKeys = this.swiftKeys.filter((key) => {
354 return key.user !== subuser.id;
356 // Remove the subuser to update the UI.
357 this.subusers.splice(index, 1);
358 // Mark the form as dirty to be able to submit it.
359 this.userForm.markAsDirty();
363 * Add/Update a capability.
365 setCapability(cap: RgwUserCapability, index?: number) {
366 const uid = this.userForm.getValue('uid');
367 if (_.isNumber(index)) {
369 const oldCap = this.capabilities[index];
370 // Note, the RadosGW Admin OPS API does not support the modification of
371 // user capabilities. Because of that it is necessary to delete it and
372 // then to re-add the capability with its new value/permission.
373 this.submitObservables.push(
374 this.rgwUserService.deleteCapability(uid, oldCap.type, oldCap.perm)
376 this.submitObservables.push(this.rgwUserService.addCapability(uid, cap.type, cap.perm));
377 this.capabilities[index] = cap;
380 // Create an observable to add the capability when the form is submitted.
381 this.submitObservables.push(this.rgwUserService.addCapability(uid, cap.type, cap.perm));
382 this.capabilities.push(cap);
384 // Mark the form as dirty to be able to submit it.
385 this.userForm.markAsDirty();
389 * Delete the given capability:
390 * - Delete it from the local array to update the UI
391 * - Create an observable that will be executed on form submit
392 * @param {number} index The capability to delete.
394 deleteCapability(index: number) {
395 const cap = this.capabilities[index];
396 // Create an observable to delete the capability when the form is submitted.
397 this.submitObservables.push(
398 this.rgwUserService.deleteCapability(this.userForm.getValue('uid'), cap.type, cap.perm)
400 // Remove the capability to update the UI.
401 this.capabilities.splice(index, 1);
402 // Mark the form as dirty to be able to submit it.
403 this.userForm.markAsDirty();
406 hasAllCapabilities() {
407 return !_.difference(RgwUserCapabilities.getAll(), _.map(this.capabilities, 'type')).length;
411 * Add/Update a S3 key.
413 setS3Key(key: RgwUserS3Key, index?: number) {
414 if (_.isNumber(index)) {
416 // Nothing to do here at the moment.
419 // Split the key's user name into its user and subuser parts.
420 const userMatches = key.user.match(/([^:]+)(:(.+))?/);
421 // Create an observable to add the S3 key when the form is submitted.
422 const uid = userMatches[1];
424 subuser: userMatches[2] ? userMatches[3] : '',
425 generate_key: key.generate_key ? 'true' : 'false'
427 if (args['generate_key'] === 'false') {
428 if (!_.isNil(key.access_key)) {
429 args['access_key'] = key.access_key;
431 if (!_.isNil(key.secret_key)) {
432 args['secret_key'] = key.secret_key;
435 this.submitObservables.push(this.rgwUserService.addS3Key(uid, args));
436 // If the access and the secret key are auto-generated, then visualize
437 // this to the user by displaying a notification instead of the key.
440 access_key: key.generate_key ? 'Apply your changes first...' : key.access_key,
441 secret_key: key.generate_key ? 'Apply your changes first...' : key.secret_key
444 // Mark the form as dirty to be able to submit it.
445 this.userForm.markAsDirty();
450 * @param {number} index The S3 key to delete.
452 deleteS3Key(index: number) {
453 const key = this.s3Keys[index];
454 // Create an observable to delete the S3 key when the form is submitted.
455 this.submitObservables.push(
456 this.rgwUserService.deleteS3Key(this.userForm.getValue('uid'), key.access_key)
458 // Remove the S3 key to update the UI.
459 this.s3Keys.splice(index, 1);
460 // Mark the form as dirty to be able to submit it.
461 this.userForm.markAsDirty();
465 * Show the specified subuser in a modal dialog.
466 * @param {number | undefined} index The subuser to show.
468 showSubuserModal(index?: number) {
469 const uid = this.userForm.getValue('uid');
470 const modalRef = this.bsModalService.show(RgwUserSubuserModalComponent);
471 if (_.isNumber(index)) {
473 const subuser = this.subusers[index];
474 modalRef.content.setEditing();
475 modalRef.content.setValues(uid, subuser.id, subuser.permissions);
478 modalRef.content.setEditing(false);
479 modalRef.content.setValues(uid);
480 modalRef.content.setSubusers(this.subusers);
482 modalRef.content.submitAction.subscribe((subuser: RgwUserSubuser) => {
483 this.setSubuser(subuser, index);
488 * Show the specified S3 key in a modal dialog.
489 * @param {number | undefined} index The S3 key to show.
491 showS3KeyModal(index?: number) {
492 const modalRef = this.bsModalService.show(RgwUserS3KeyModalComponent);
493 if (_.isNumber(index)) {
495 const key = this.s3Keys[index];
496 modalRef.content.setViewing();
497 modalRef.content.setValues(key.user, key.access_key, key.secret_key);
500 const candidates = this._getS3KeyUserCandidates();
501 modalRef.content.setViewing(false);
502 modalRef.content.setUserCandidates(candidates);
503 modalRef.content.submitAction.subscribe((key: RgwUserS3Key) => {
510 * Show the specified Swift key in a modal dialog.
511 * @param {number} index The Swift key to show.
513 showSwiftKeyModal(index: number) {
514 const modalRef = this.bsModalService.show(RgwUserSwiftKeyModalComponent);
515 const key = this.swiftKeys[index];
516 modalRef.content.setValues(key.user, key.secret_key);
520 * Show the specified capability in a modal dialog.
521 * @param {number | undefined} index The S3 key to show.
523 showCapabilityModal(index?: number) {
524 const modalRef = this.bsModalService.show(RgwUserCapabilityModalComponent);
525 if (_.isNumber(index)) {
527 const cap = this.capabilities[index];
528 modalRef.content.setEditing();
529 modalRef.content.setValues(cap.type, cap.perm);
532 modalRef.content.setEditing(false);
533 modalRef.content.setCapabilities(this.capabilities);
535 modalRef.content.submitAction.subscribe((cap: RgwUserCapability) => {
536 this.setCapability(cap, index);
541 * Check if the general user settings (display name, email, ...) have been modified.
542 * @return {Boolean} Returns TRUE if the general user settings have been modified.
544 private _isGeneralDirty(): boolean {
545 return ['display_name', 'email', 'max_buckets_mode', 'max_buckets', 'suspended'].some(
547 return this.userForm.get(path).dirty;
553 * Check if the user quota has been modified.
554 * @return {Boolean} Returns TRUE if the user quota has been modified.
556 private _isUserQuotaDirty(): boolean {
558 'user_quota_enabled',
559 'user_quota_max_size_unlimited',
560 'user_quota_max_size',
561 'user_quota_max_objects_unlimited',
562 'user_quota_max_objects'
564 return this.userForm.get(path).dirty;
569 * Check if the bucket quota has been modified.
570 * @return {Boolean} Returns TRUE if the bucket quota has been modified.
572 private _isBucketQuotaDirty(): boolean {
574 'bucket_quota_enabled',
575 'bucket_quota_max_size_unlimited',
576 'bucket_quota_max_size',
577 'bucket_quota_max_objects_unlimited',
578 'bucket_quota_max_objects'
580 return this.userForm.get(path).dirty;
585 * Helper function to get the arguments of the API request when a new
588 private _getCreateArgs() {
590 uid: this.userForm.getValue('uid'),
591 display_name: this.userForm.getValue('display_name'),
592 suspended: this.userForm.getValue('suspended'),
594 max_buckets: this.userForm.getValue('max_buckets'),
595 generate_key: this.userForm.getValue('generate_key'),
599 const email = this.userForm.getValue('email');
600 if (_.isString(email) && email.length > 0) {
601 _.merge(result, { email: email });
603 const generateKey = this.userForm.getValue('generate_key');
607 access_key: this.userForm.getValue('access_key'),
608 secret_key: this.userForm.getValue('secret_key')
611 const maxBucketsMode = parseInt(this.userForm.getValue('max_buckets_mode'), 10);
612 if (_.includes([-1, 0], maxBucketsMode)) {
613 // -1 => Disable bucket creation.
614 // 0 => Unlimited bucket creation.
615 _.merge(result, { max_buckets: maxBucketsMode });
621 * Helper function to get the arguments for the API request when the user
622 * configuration has been modified.
624 private _getUpdateArgs() {
625 const result: Record<string, any> = {};
626 const keys = ['display_name', 'email', 'max_buckets', 'suspended'];
627 for (const key of keys) {
628 result[key] = this.userForm.getValue(key);
630 const maxBucketsMode = parseInt(this.userForm.getValue('max_buckets_mode'), 10);
631 if (_.includes([-1, 0], maxBucketsMode)) {
632 // -1 => Disable bucket creation.
633 // 0 => Unlimited bucket creation.
634 result['max_buckets'] = maxBucketsMode;
640 * Helper function to get the arguments for the API request when the user
641 * quota configuration has been modified.
643 private _getUserQuotaArgs(): Record<string, any> {
646 enabled: this.userForm.getValue('user_quota_enabled'),
650 if (!this.userForm.getValue('user_quota_max_size_unlimited')) {
651 // Convert the given value to bytes.
652 const bytes = new FormatterService().toBytes(this.userForm.getValue('user_quota_max_size'));
653 // Finally convert the value to KiB.
654 result['max_size_kb'] = (bytes / 1024).toFixed(0) as any;
656 if (!this.userForm.getValue('user_quota_max_objects_unlimited')) {
657 result['max_objects'] = this.userForm.getValue('user_quota_max_objects');
663 * Helper function to get the arguments for the API request when the bucket
664 * quota configuration has been modified.
666 private _getBucketQuotaArgs(): Record<string, any> {
668 quota_type: 'bucket',
669 enabled: this.userForm.getValue('bucket_quota_enabled'),
673 if (!this.userForm.getValue('bucket_quota_max_size_unlimited')) {
674 // Convert the given value to bytes.
675 const bytes = new FormatterService().toBytes(this.userForm.getValue('bucket_quota_max_size'));
676 // Finally convert the value to KiB.
677 result['max_size_kb'] = (bytes / 1024).toFixed(0) as any;
679 if (!this.userForm.getValue('bucket_quota_max_objects_unlimited')) {
680 result['max_objects'] = this.userForm.getValue('bucket_quota_max_objects');
686 * Helper method to get the user candidates for S3 keys.
687 * @returns {Array} Returns a list of user identifiers.
689 private _getS3KeyUserCandidates() {
691 // Add the current user id.
692 const uid = this.userForm.getValue('uid');
693 if (_.isString(uid) && !_.isEmpty(uid)) {
696 // Append the subusers.
697 this.subusers.forEach((subUser) => {
698 result.push(subUser.id);
700 // Note that it's possible to create multiple S3 key pairs for a user,
701 // thus we append already configured users, too.
702 this.s3Keys.forEach((key) => {
703 result.push(key.user);
705 result = _.uniq(result);