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 { 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 { NotificationType } from '../../../shared/enum/notification-type.enum';
13 import { CdFormBuilder } from '../../../shared/forms/cd-form-builder';
14 import { CdFormGroup } from '../../../shared/forms/cd-form-group';
15 import { CdValidators, isEmptyInputValue } from '../../../shared/forms/cd-validators';
16 import { FormatterService } from '../../../shared/services/formatter.service';
17 import { NotificationService } from '../../../shared/services/notification.service';
18 import { RgwUserCapability } from '../models/rgw-user-capability';
19 import { RgwUserS3Key } from '../models/rgw-user-s3-key';
20 import { RgwUserSubuser } from '../models/rgw-user-subuser';
21 import { RgwUserSwiftKey } from '../models/rgw-user-swift-key';
22 import { RgwUserCapabilityModalComponent } from '../rgw-user-capability-modal/rgw-user-capability-modal.component';
23 import { RgwUserS3KeyModalComponent } from '../rgw-user-s3-key-modal/rgw-user-s3-key-modal.component';
24 import { RgwUserSubuserModalComponent } from '../rgw-user-subuser-modal/rgw-user-subuser-modal.component';
25 import { RgwUserSwiftKeyModalComponent } from '../rgw-user-swift-key-modal/rgw-user-swift-key-modal.component';
28 selector: 'cd-rgw-user-form',
29 templateUrl: './rgw-user-form.component.html',
30 styleUrls: ['./rgw-user-form.component.scss']
32 export class RgwUserFormComponent implements OnInit {
33 userForm: CdFormGroup;
37 submitObservables: Observable<Object>[] = [];
39 subusers: RgwUserSubuser[] = [];
40 s3Keys: RgwUserS3Key[] = [];
41 swiftKeys: RgwUserSwiftKey[] = [];
42 capabilities: RgwUserCapability[] = [];
48 capabilityLabel: string;
51 private formBuilder: CdFormBuilder,
52 private route: ActivatedRoute,
53 private router: Router,
54 private rgwUserService: RgwUserService,
55 private bsModalService: BsModalService,
56 private notificationService: NotificationService,
58 public actionLabels: ActionLabelsI18n
60 this.resource = this.i18n('user');
61 this.subuserLabel = this.i18n('subuser');
62 this.s3keyLabel = this.i18n('S3 Key');
63 this.capabilityLabel = this.i18n('capability');
65 this.listenToChanges();
69 this.userForm = this.formBuilder.group({
73 [Validators.required],
74 [CdValidators.unique(this.rgwUserService.exists, this.rgwUserService)]
76 display_name: [null, [Validators.required]],
80 [CdValidators.unique(this.rgwUserService.emailExists, this.rgwUserService)]
82 max_buckets: [1000, [Validators.required, Validators.min(0)]],
86 access_key: [null, [CdValidators.requiredIf({ generate_key: false })]],
87 secret_key: [null, [CdValidators.requiredIf({ generate_key: false })]],
89 user_quota_enabled: [false],
90 user_quota_max_size_unlimited: [true],
91 user_quota_max_size: [
94 CdValidators.requiredIf({
95 user_quota_enabled: true,
96 user_quota_max_size_unlimited: false
98 this.quotaMaxSizeValidator
101 user_quota_max_objects_unlimited: [true],
102 user_quota_max_objects: [
106 CdValidators.requiredIf({
107 user_quota_enabled: true,
108 user_quota_max_objects_unlimited: false
113 bucket_quota_enabled: [false],
114 bucket_quota_max_size_unlimited: [true],
115 bucket_quota_max_size: [
118 CdValidators.requiredIf({
119 bucket_quota_enabled: true,
120 bucket_quota_max_size_unlimited: false
122 this.quotaMaxSizeValidator
125 bucket_quota_max_objects_unlimited: [true],
126 bucket_quota_max_objects: [
130 CdValidators.requiredIf({
131 bucket_quota_enabled: true,
132 bucket_quota_max_objects_unlimited: false
140 // Reset the validation status of various controls, especially those that are using
141 // the 'requiredIf' validator. This is necessary because the controls itself are not
142 // validated again if the status of their prerequisites have been changed.
143 this.userForm.get('generate_key').valueChanges.subscribe(() => {
144 ['access_key', 'secret_key'].forEach((path) => {
145 this.userForm.get(path).updateValueAndValidity({ onlySelf: true });
148 this.userForm.get('user_quota_enabled').valueChanges.subscribe(() => {
149 ['user_quota_max_size', 'user_quota_max_objects'].forEach((path) => {
150 this.userForm.get(path).updateValueAndValidity({ onlySelf: true });
153 this.userForm.get('user_quota_max_size_unlimited').valueChanges.subscribe(() => {
154 this.userForm.get('user_quota_max_size').updateValueAndValidity({ onlySelf: true });
156 this.userForm.get('user_quota_max_objects_unlimited').valueChanges.subscribe(() => {
157 this.userForm.get('user_quota_max_objects').updateValueAndValidity({ onlySelf: true });
159 this.userForm.get('bucket_quota_enabled').valueChanges.subscribe(() => {
160 ['bucket_quota_max_size', 'bucket_quota_max_objects'].forEach((path) => {
161 this.userForm.get(path).updateValueAndValidity({ onlySelf: true });
164 this.userForm.get('bucket_quota_max_size_unlimited').valueChanges.subscribe(() => {
165 this.userForm.get('bucket_quota_max_size').updateValueAndValidity({ onlySelf: true });
167 this.userForm.get('bucket_quota_max_objects_unlimited').valueChanges.subscribe(() => {
168 this.userForm.get('bucket_quota_max_objects').updateValueAndValidity({ onlySelf: true });
173 this.editing = this.router.url.startsWith(`/rgw/user/${URLVerbs.EDIT}`);
174 this.action = this.editing ? this.actionLabels.EDIT : this.actionLabels.CREATE;
175 // Process route parameters.
176 this.route.params.subscribe((params: { uid: string }) => {
177 if (!params.hasOwnProperty('uid')) {
180 const uid = decodeURIComponent(params.uid);
182 // Load the user and quota information.
183 const observables = [];
184 observables.push(this.rgwUserService.get(uid));
185 observables.push(this.rgwUserService.getQuota(uid));
186 observableForkJoin(observables).subscribe(
188 this.loading = false;
189 // Get the default values.
190 const defaults = _.clone(this.userForm.value);
191 // Extract the values displayed in the form.
192 let value = _.pick(resp[0], _.keys(this.userForm.value));
193 // Map the quota values.
194 ['user', 'bucket'].forEach((type) => {
195 const quota = resp[1][type + '_quota'];
196 value[type + '_quota_enabled'] = quota.enabled;
197 if (quota.max_size < 0) {
198 value[type + '_quota_max_size_unlimited'] = true;
199 value[type + '_quota_max_size'] = null;
201 value[type + '_quota_max_size_unlimited'] = false;
202 value[type + '_quota_max_size'] = `${quota.max_size} B`;
204 if (quota.max_objects < 0) {
205 value[type + '_quota_max_objects_unlimited'] = true;
206 value[type + '_quota_max_objects'] = null;
208 value[type + '_quota_max_objects_unlimited'] = false;
209 value[type + '_quota_max_objects'] = quota.max_objects;
212 // Merge with default values.
213 value = _.merge(defaults, value);
215 this.userForm.setValue(value);
217 // Get the sub users.
218 this.subusers = resp[0].subusers;
221 this.s3Keys = resp[0].keys;
222 this.swiftKeys = resp[0].swift_keys;
224 // Process the capabilities.
225 const mapPerm = { 'read, write': '*' };
226 resp[0].caps.forEach((cap) => {
227 if (cap.perm in mapPerm) {
228 cap.perm = mapPerm[cap.perm];
231 this.capabilities = resp[0].caps;
241 this.router.navigate(['/rgw/user']);
245 let notificationTitle: string;
246 // Exit immediately if the form isn't dirty.
247 if (this.userForm.pristine) {
251 const uid = this.userForm.getValue('uid');
254 if (this._isGeneralDirty()) {
255 const args = this._getUpdateArgs();
256 this.submitObservables.push(this.rgwUserService.update(uid, args));
258 notificationTitle = this.i18n('Updated Object Gateway user "{{uid}}"', { uid: uid });
261 const args = this._getCreateArgs();
262 this.submitObservables.push(this.rgwUserService.create(args));
263 notificationTitle = this.i18n('Created Object Gateway user "{{uid}}"', { uid: uid });
265 // Check if user quota has been modified.
266 if (this._isUserQuotaDirty()) {
267 const userQuotaArgs = this._getUserQuotaArgs();
268 this.submitObservables.push(this.rgwUserService.updateQuota(uid, userQuotaArgs));
270 // Check if bucket quota has been modified.
271 if (this._isBucketQuotaDirty()) {
272 const bucketQuotaArgs = this._getBucketQuotaArgs();
273 this.submitObservables.push(this.rgwUserService.updateQuota(uid, bucketQuotaArgs));
275 // Finally execute all observables.
276 observableForkJoin(this.submitObservables).subscribe(
278 this.notificationService.show(NotificationType.success, notificationTitle);
282 // Reset the 'Submit' button.
283 this.userForm.setErrors({ cdSubmitButton: true });
289 * Validate the quota maximum size, e.g. 1096, 1K, 30M. Only integer numbers are valid,
290 * something like 1.9M is not recognized as valid.
292 quotaMaxSizeValidator(control: AbstractControl): ValidationErrors | null {
293 if (isEmptyInputValue(control.value)) {
296 const m = RegExp('^(\\d+)\\s*(B|K(B|iB)?|M(B|iB)?|G(B|iB)?|T(B|iB)?)?$', 'i').exec(
300 return { quotaMaxSize: true };
302 const bytes = new FormatterService().toBytes(control.value);
303 return bytes < 1024 ? { quotaMaxSize: true } : null;
307 * Add/Update a subuser.
309 setSubuser(subuser: RgwUserSubuser, index?: number) {
310 const mapPermissions = {
311 'full-control': 'full',
312 'read-write': 'readwrite'
314 const uid = this.userForm.getValue('uid');
318 subuser.permissions in mapPermissions
319 ? mapPermissions[subuser.permissions]
320 : subuser.permissions,
322 secret_key: subuser.secret_key,
323 generate_secret: subuser.generate_secret ? 'true' : 'false'
325 this.submitObservables.push(this.rgwUserService.createSubuser(uid, args));
326 if (_.isNumber(index)) {
328 // Create an observable to modify the subuser when the form is submitted.
329 this.subusers[index] = subuser;
332 // Create an observable to add the subuser when the form is submitted.
333 this.subusers.push(subuser);
334 // Add a Swift key. If the secret key is auto-generated, then visualize
335 // this to the user by displaying a notification instead of the key.
336 this.swiftKeys.push({
338 secret_key: subuser.generate_secret ? 'Apply your changes first...' : subuser.secret_key
341 // Mark the form as dirty to be able to submit it.
342 this.userForm.markAsDirty();
347 * @param {number} index The subuser to delete.
349 deleteSubuser(index: number) {
350 const subuser = this.subusers[index];
351 // Create an observable to delete the subuser when the form is submitted.
352 this.submitObservables.push(
353 this.rgwUserService.deleteSubuser(this.userForm.getValue('uid'), subuser.id)
355 // Remove the associated S3 keys.
356 this.s3Keys = this.s3Keys.filter((key) => {
357 return key.user !== subuser.id;
359 // Remove the associated Swift keys.
360 this.swiftKeys = this.swiftKeys.filter((key) => {
361 return key.user !== subuser.id;
363 // Remove the subuser to update the UI.
364 this.subusers.splice(index, 1);
365 // Mark the form as dirty to be able to submit it.
366 this.userForm.markAsDirty();
370 * Add/Update a capability.
372 setCapability(cap: RgwUserCapability, index?: number) {
373 const uid = this.userForm.getValue('uid');
374 if (_.isNumber(index)) {
376 const oldCap = this.capabilities[index];
377 // Note, the RadosGW Admin OPS API does not support the modification of
378 // user capabilities. Because of that it is necessary to delete it and
379 // then to re-add the capability with its new value/permission.
380 this.submitObservables.push(
381 this.rgwUserService.deleteCapability(uid, oldCap.type, oldCap.perm)
383 this.submitObservables.push(this.rgwUserService.addCapability(uid, cap.type, cap.perm));
384 this.capabilities[index] = cap;
387 // Create an observable to add the capability when the form is submitted.
388 this.submitObservables.push(this.rgwUserService.addCapability(uid, cap.type, cap.perm));
389 this.capabilities.push(cap);
391 // Mark the form as dirty to be able to submit it.
392 this.userForm.markAsDirty();
396 * Delete the given capability:
397 * - Delete it from the local array to update the UI
398 * - Create an observable that will be executed on form submit
399 * @param {number} index The capability to delete.
401 deleteCapability(index: number) {
402 const cap = this.capabilities[index];
403 // Create an observable to delete the capability when the form is submitted.
404 this.submitObservables.push(
405 this.rgwUserService.deleteCapability(this.userForm.getValue('uid'), cap.type, cap.perm)
407 // Remove the capability to update the UI.
408 this.capabilities.splice(index, 1);
409 // Mark the form as dirty to be able to submit it.
410 this.userForm.markAsDirty();
414 * Add/Update a S3 key.
416 setS3Key(key: RgwUserS3Key, index?: number) {
417 if (_.isNumber(index)) {
419 // Nothing to do here at the moment.
422 // Split the key's user name into its user and subuser parts.
423 const userMatches = key.user.match(/([^:]+)(:(.+))?/);
424 // Create an observable to add the S3 key when the form is submitted.
425 const uid = userMatches[1];
427 subuser: userMatches[2] ? userMatches[3] : '',
428 generate_key: key.generate_key ? 'true' : 'false',
429 access_key: key.access_key,
430 secret_key: key.secret_key
432 this.submitObservables.push(this.rgwUserService.addS3Key(uid, args));
433 // If the access and the secret key are auto-generated, then visualize
434 // this to the user by displaying a notification instead of the key.
437 access_key: key.generate_key ? 'Apply your changes first...' : key.access_key,
438 secret_key: key.generate_key ? 'Apply your changes first...' : key.secret_key
441 // Mark the form as dirty to be able to submit it.
442 this.userForm.markAsDirty();
447 * @param {number} index The S3 key to delete.
449 deleteS3Key(index: number) {
450 const key = this.s3Keys[index];
451 // Create an observable to delete the S3 key when the form is submitted.
452 this.submitObservables.push(
453 this.rgwUserService.deleteS3Key(this.userForm.getValue('uid'), key.access_key)
455 // Remove the S3 key to update the UI.
456 this.s3Keys.splice(index, 1);
457 // Mark the form as dirty to be able to submit it.
458 this.userForm.markAsDirty();
462 * Show the specified subuser in a modal dialog.
463 * @param {number | undefined} index The subuser to show.
465 showSubuserModal(index?: number) {
466 const uid = this.userForm.getValue('uid');
467 const modalRef = this.bsModalService.show(RgwUserSubuserModalComponent);
468 if (_.isNumber(index)) {
470 const subuser = this.subusers[index];
471 modalRef.content.setEditing();
472 modalRef.content.setValues(uid, subuser.id, subuser.permissions);
475 modalRef.content.setEditing(false);
476 modalRef.content.setValues(uid);
477 modalRef.content.setSubusers(this.subusers);
479 modalRef.content.submitAction.subscribe((subuser: RgwUserSubuser) => {
480 this.setSubuser(subuser, index);
485 * Show the specified S3 key in a modal dialog.
486 * @param {number | undefined} index The S3 key to show.
488 showS3KeyModal(index?: number) {
489 const modalRef = this.bsModalService.show(RgwUserS3KeyModalComponent);
490 if (_.isNumber(index)) {
492 const key = this.s3Keys[index];
493 modalRef.content.setViewing();
494 modalRef.content.setValues(key.user, key.access_key, key.secret_key);
497 const candidates = this._getS3KeyUserCandidates();
498 modalRef.content.setViewing(false);
499 modalRef.content.setUserCandidates(candidates);
500 modalRef.content.submitAction.subscribe((key: RgwUserS3Key) => {
507 * Show the specified Swift key in a modal dialog.
508 * @param {number} index The Swift key to show.
510 showSwiftKeyModal(index: number) {
511 const modalRef = this.bsModalService.show(RgwUserSwiftKeyModalComponent);
512 const key = this.swiftKeys[index];
513 modalRef.content.setValues(key.user, key.secret_key);
517 * Show the specified capability in a modal dialog.
518 * @param {number | undefined} index The S3 key to show.
520 showCapabilityModal(index?: number) {
521 const modalRef = this.bsModalService.show(RgwUserCapabilityModalComponent);
522 if (_.isNumber(index)) {
524 const cap = this.capabilities[index];
525 modalRef.content.setEditing();
526 modalRef.content.setValues(cap.type, cap.perm);
529 modalRef.content.setEditing(false);
530 modalRef.content.setCapabilities(this.capabilities);
532 modalRef.content.submitAction.subscribe((cap: RgwUserCapability) => {
533 this.setCapability(cap, index);
538 * Check if the general user settings (display name, email, ...) have been modified.
539 * @return {Boolean} Returns TRUE if the general user settings have been modified.
541 private _isGeneralDirty(): boolean {
542 return ['display_name', 'email', 'max_buckets', 'suspended'].some((path) => {
543 return this.userForm.get(path).dirty;
548 * Check if the user quota has been modified.
549 * @return {Boolean} Returns TRUE if the user quota has been modified.
551 private _isUserQuotaDirty(): boolean {
553 'user_quota_enabled',
554 'user_quota_max_size_unlimited',
555 'user_quota_max_size',
556 'user_quota_max_objects_unlimited',
557 'user_quota_max_objects'
559 return this.userForm.get(path).dirty;
564 * Check if the bucket quota has been modified.
565 * @return {Boolean} Returns TRUE if the bucket quota has been modified.
567 private _isBucketQuotaDirty(): boolean {
569 'bucket_quota_enabled',
570 'bucket_quota_max_size_unlimited',
571 'bucket_quota_max_size',
572 'bucket_quota_max_objects_unlimited',
573 'bucket_quota_max_objects'
575 return this.userForm.get(path).dirty;
580 * Helper function to get the arguments of the API request when a new
583 private _getCreateArgs() {
585 uid: this.userForm.getValue('uid'),
586 display_name: this.userForm.getValue('display_name'),
587 suspended: this.userForm.getValue('suspended'),
589 max_buckets: this.userForm.getValue('max_buckets'),
590 generate_key: this.userForm.getValue('generate_key'),
594 const email = this.userForm.getValue('email');
595 if (_.isString(email) && email.length > 0) {
596 _.merge(result, { email: email });
598 const generateKey = this.userForm.getValue('generate_key');
602 access_key: this.userForm.getValue('access_key'),
603 secret_key: this.userForm.getValue('secret_key')
610 * Helper function to get the arguments for the API request when the user
611 * configuration has been modified.
613 private _getUpdateArgs() {
615 const keys = ['display_name', 'email', 'max_buckets', 'suspended'];
616 for (const key of keys) {
617 result[key] = this.userForm.getValue(key);
623 * Helper function to get the arguments for the API request when the user
624 * quota configuration has been modified.
626 private _getUserQuotaArgs(): object {
629 enabled: this.userForm.getValue('user_quota_enabled'),
633 if (!this.userForm.getValue('user_quota_max_size_unlimited')) {
634 // Convert the given value to bytes.
635 const bytes = new FormatterService().toBytes(this.userForm.getValue('user_quota_max_size'));
636 // Finally convert the value to KiB.
637 result['max_size_kb'] = (bytes / 1024).toFixed(0) as any;
639 if (!this.userForm.getValue('user_quota_max_objects_unlimited')) {
640 result['max_objects'] = this.userForm.getValue('user_quota_max_objects');
646 * Helper function to get the arguments for the API request when the bucket
647 * quota configuration has been modified.
649 private _getBucketQuotaArgs(): object {
651 quota_type: 'bucket',
652 enabled: this.userForm.getValue('bucket_quota_enabled'),
656 if (!this.userForm.getValue('bucket_quota_max_size_unlimited')) {
657 // Convert the given value to bytes.
658 const bytes = new FormatterService().toBytes(this.userForm.getValue('bucket_quota_max_size'));
659 // Finally convert the value to KiB.
660 result['max_size_kb'] = (bytes / 1024).toFixed(0) as any;
662 if (!this.userForm.getValue('bucket_quota_max_objects_unlimited')) {
663 result['max_objects'] = this.userForm.getValue('bucket_quota_max_objects');
669 * Helper method to get the user candidates for S3 keys.
670 * @returns {Array} Returns a list of user identifiers.
672 private _getS3KeyUserCandidates() {
674 // Add the current user id.
675 const uid = this.userForm.getValue('uid');
676 if (_.isString(uid) && !_.isEmpty(uid)) {
679 // Append the subusers.
680 this.subusers.forEach((subUser) => {
681 result.push(subUser.id);
683 // Note that it's possible to create multiple S3 key pairs for a user,
684 // thus we append already configured users, too.
685 this.s3Keys.forEach((key) => {
686 result.push(key.user);
688 result = _.uniq(result);