From d9bbe41a99738fa9a81136269e6902448d8b185a Mon Sep 17 00:00:00 2001 From: Fabricio Duarte Date: Sun, 16 Aug 2026 21:46:26 -0300 Subject: [PATCH 1/3] Revert registerUserKeys back to a synchronous command --- .../apache/cloudstack/api/BaseAsyncCmd.java | 1 - .../command/admin/user/DeleteUserKeysCmd.java | 23 ++------------- .../admin/user/RegisterUserKeysCmd.java | 28 ++----------------- .../com/cloud/user/AccountManagerImpl.java | 1 + ui/src/components/view/ApiKeyPairsTab.vue | 27 ++++-------------- ui/src/views/iam/GenerateApiKeyPair.vue | 18 ++++-------- 6 files changed, 16 insertions(+), 82 deletions(-) diff --git a/api/src/main/java/org/apache/cloudstack/api/BaseAsyncCmd.java b/api/src/main/java/org/apache/cloudstack/api/BaseAsyncCmd.java index c67c5a023e09..6859b0a7f406 100644 --- a/api/src/main/java/org/apache/cloudstack/api/BaseAsyncCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/BaseAsyncCmd.java @@ -29,7 +29,6 @@ public abstract class BaseAsyncCmd extends BaseCmd { public static final String migrationSyncObject = "migration"; public static final String snapshotHostSyncObject = "snapshothost"; public static final String gslbSyncObject = "globalserverloadbalancer"; - public static final String user = "user"; private Object job; diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/user/DeleteUserKeysCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/user/DeleteUserKeysCmd.java index 6cf55514ba36..dec41530c48a 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/user/DeleteUserKeysCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/user/DeleteUserKeysCmd.java @@ -16,21 +16,20 @@ // under the License. package org.apache.cloudstack.api.command.admin.user; -import com.cloud.event.EventTypes; import com.cloud.user.Account; import org.apache.cloudstack.acl.apikeypair.ApiKeyPair; import org.apache.cloudstack.api.ACL; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiCommandResourceType; import org.apache.cloudstack.api.ApiConstants; -import org.apache.cloudstack.api.BaseAsyncCmd; +import org.apache.cloudstack.api.BaseCmd; import org.apache.cloudstack.api.Parameter; import org.apache.cloudstack.api.response.ApiKeyPairResponse; import org.apache.cloudstack.api.response.SuccessResponse; @APICommand(name = "deleteUserKeys", description = "Deletes a keypair from a user", responseObject = SuccessResponse.class, since = "4.23.0", requestHasSensitiveInfo = false, responseHasSensitiveInfo = false) -public class DeleteUserKeysCmd extends BaseAsyncCmd { +public class DeleteUserKeysCmd extends BaseCmd { @ACL @Parameter(name = ApiConstants.KEYPAIR_ID, type = CommandType.UUID, entityType = ApiKeyPairResponse.class, required = true, description = "ID of the keypair to be deleted.") private Long id; @@ -60,22 +59,4 @@ public void execute() { SuccessResponse response = new SuccessResponse(getCommandName()); this.setResponseObject(response); } - - @Override - public String getEventType() { - return EventTypes.EVENT_DELETE_SECRET_API_KEY; - } - - @Override - public String getEventDescription() { - ApiKeyPair keyPair = apiKeyPairService.findById(id); - return String.format("Deleting API key pair with ID [%s]%s", - keyPair == null ? id : keyPair.getUuid(), - keyPair == null ? "." : String.format(" and name [%s].", keyPair.getName())); - } - - @Override - public Long getSyncObjId() { - return getId(); - } } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/user/RegisterUserKeysCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/user/RegisterUserKeysCmd.java index 28c79517f4b9..96ffc7897045 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/user/RegisterUserKeysCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/user/RegisterUserKeysCmd.java @@ -16,14 +16,13 @@ // under the License. package org.apache.cloudstack.api.command.admin.user; -import com.cloud.event.EventTypes; import com.cloud.user.Account; import com.cloud.user.User; import org.apache.cloudstack.acl.Rule; import org.apache.cloudstack.acl.apikeypair.ApiKeyPair; import org.apache.cloudstack.api.ApiCommandResourceType; import org.apache.cloudstack.api.ApiErrorCode; -import org.apache.cloudstack.api.BaseAsyncCmd; +import org.apache.cloudstack.api.BaseCmd; import org.apache.cloudstack.api.ServerApiException; import org.apache.commons.lang3.StringUtils; @@ -43,7 +42,7 @@ responseObject = ApiKeyPairResponse.class, description = "Registers an API key pair (API and secret keys) for a user.", requestHasSensitiveInfo = false, responseHasSensitiveInfo = true) -public class RegisterUserKeysCmd extends BaseAsyncCmd { +public class RegisterUserKeysCmd extends BaseCmd { @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = UserResponse.class, required = true, description = "ID of the user.") private Long id; @@ -61,7 +60,7 @@ public class RegisterUserKeysCmd extends BaseAsyncCmd { ApiConstants.PARAMETER_DESCRIPTION_END_DATE_POSSIBLE_FORMATS) private Date endDate; - @Parameter(name = ApiConstants.RULES, type = CommandType.MAP, description = "The rules of the API key pair. If no rules are informed, " + + @Parameter(name = ApiConstants.RULES, type = BaseCmd.CommandType.MAP, description = "The rules of the API key pair. If no rules are informed, " + "defaults to allowing all account permissions. Otherwise, only the explicitly informed permissions for the key pair will be " + "considered. Lower indexed rules take precedence over higher. Thus, in the following example: " + "\"rules[0].rule=deleteUserKeys rules[0].permission=deny rules[1].rule=*UserKey* rules[1].permission=allow\", all rules matching " + @@ -188,25 +187,4 @@ public void execute() { response.setResponseName(getCommandName()); this.setResponseObject(response); } - - @Override - public String getEventType() { - return EventTypes.EVENT_REGISTER_FOR_SECRET_API_KEY; - } - - @Override - public String getEventDescription() { - String userUuid = getResourceUuid(ApiConstants.ID); - return String.format("Registering API keypair for user [%s].", userUuid == null ? id : userUuid); - } - - @Override - public String getSyncObjType() { - return BaseAsyncCmd.user; - } - - @Override - public Long getSyncObjId() { - return getUserId(); - } } diff --git a/server/src/main/java/com/cloud/user/AccountManagerImpl.java b/server/src/main/java/com/cloud/user/AccountManagerImpl.java index db9c1d1dafde..8551cfcfdc17 100644 --- a/server/src/main/java/com/cloud/user/AccountManagerImpl.java +++ b/server/src/main/java/com/cloud/user/AccountManagerImpl.java @@ -3399,6 +3399,7 @@ private void removeApiKeyPairIfExpired(ApiKeyPair apiKeyPair) { } } + @ActionEvent(eventType = EventTypes.EVENT_DELETE_SECRET_API_KEY, eventDescription = "deleting API key pair") public void deleteApiKey(DeleteUserKeysCmd cmd) { ApiKeyPair keyPair = apiKeyPairService.findById(cmd.getId()); if (keyPair == null) { diff --git a/ui/src/components/view/ApiKeyPairsTab.vue b/ui/src/components/view/ApiKeyPairsTab.vue index 87feda59d992..5e426cba683d 100644 --- a/ui/src/components/view/ApiKeyPairsTab.vue +++ b/ui/src/components/view/ApiKeyPairsTab.vue @@ -287,34 +287,17 @@ export default { this.fetchLoading = true try { await Promise.all(keypairs.map(async keypair => { - try { - const jobId = await this.deleteKeyPair({ - keypairid: keypair.id - }) - await this.$pollJob({ - jobId, - action: { - isFetchData: false - }, - successMethod: () => { - eventBus.emit('update-resource-state', { selectedItems: this.selectedItems, resource: keypair.id, state: 'success' }) - }, - catchMethod: () => { - eventBus.emit('update-resource-state', { selectedItems: this.selectedItems, resource: keypair.id, state: 'failed' }) - } - }) - } catch (e) { + await postAPI('deleteUserKeys', { keypairid: keypair.id }).then(response => { + eventBus.emit('update-resource-state', { selectedItems: this.selectedItems, resource: keypair.id, state: 'success' }) + }).catch(error => { eventBus.emit('update-resource-state', { selectedItems: this.selectedItems, resource: keypair.id, state: 'failed' }) - } + this.$notifyError(error) + }) })) } finally { this.fetchLoading = false } }, - async deleteKeyPair (args) { - const response = await postAPI('deleteUserKeys', args) - return response.deleteuserkeysresponse.jobid - }, bulkActionConfirmation () { this.showConfirmationAction = true this.selectedColumns = this.columns.filter(column => { diff --git a/ui/src/views/iam/GenerateApiKeyPair.vue b/ui/src/views/iam/GenerateApiKeyPair.vue index bc0b2bb475e6..9e0d307c455f 100644 --- a/ui/src/views/iam/GenerateApiKeyPair.vue +++ b/ui/src/views/iam/GenerateApiKeyPair.vue @@ -166,19 +166,12 @@ export default { const params = this.buildRequestParams() this.loading = true postAPI('registerUserKeys', params).then(response => { - this.$pollJob({ - jobId: response.registeruserkeysresponse.jobid, - successMessage: this.$t('message.success.register.user.keypair', { user: this.resource.username }), - successMethod: () => { - this.fetchData() - }, - errorMessage: this.$t('message.register.keypair.failed'), - errorMethod: () => { - this.fetchData() - }, - loadingMessage: this.$t('label.registering.keypair', { user: this.resource.username }), - catchMessage: this.$t('error.fetching.async.job.result') + this.$notification.success({ + message: this.$t('label.action.create.api.key'), + description: this.$t('message.success.register.user.keypair', { user: this.resource.username }) }) + this.fetchData() + this.closeModal() }).catch(error => { this.$notification.error({ message: this.$t('message.request.failed'), @@ -187,7 +180,6 @@ export default { }) }).finally(() => { this.loading = false - this.closeModal() }) }) }, From 8155924dc97d46df737ee49dc89f5068c9c77fc0 Mon Sep 17 00:00:00 2001 From: Fabricio Duarte Date: Sun, 16 Aug 2026 22:08:41 -0300 Subject: [PATCH 2/3] A few more small adjustments --- .../api/command/admin/user/RegisterUserKeysCmd.java | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/user/RegisterUserKeysCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/user/RegisterUserKeysCmd.java index 96ffc7897045..42c66470ac0b 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/user/RegisterUserKeysCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/user/RegisterUserKeysCmd.java @@ -46,25 +46,26 @@ public class RegisterUserKeysCmd extends BaseCmd { @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = UserResponse.class, required = true, description = "ID of the user.") private Long id; - @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, description = "API key pair name.") + @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, description = "API key pair name.", since = "4.23.0") private String name; - @Parameter(name = ApiConstants.DESCRIPTION, type = CommandType.STRING, description = "API key pair description.", length = 1024) + @Parameter(name = ApiConstants.DESCRIPTION, type = CommandType.STRING, description = "API key pair description.", length = 1024, + since = "4.23.0") private String description; @Parameter(name = ApiConstants.START_DATE, type = CommandType.DATE, description = "Start date of the API key pair. " + - ApiConstants.PARAMETER_DESCRIPTION_START_DATE_POSSIBLE_FORMATS) + ApiConstants.PARAMETER_DESCRIPTION_START_DATE_POSSIBLE_FORMATS, since = "4.23.0") private Date startDate; @Parameter(name = ApiConstants.END_DATE, type = CommandType.DATE, description = "Expiration date of the API key pair. " + - ApiConstants.PARAMETER_DESCRIPTION_END_DATE_POSSIBLE_FORMATS) + ApiConstants.PARAMETER_DESCRIPTION_END_DATE_POSSIBLE_FORMATS, since = "4.23.0") private Date endDate; - @Parameter(name = ApiConstants.RULES, type = BaseCmd.CommandType.MAP, description = "The rules of the API key pair. If no rules are informed, " + + @Parameter(name = ApiConstants.RULES, type = CommandType.MAP, description = "The rules of the API key pair. If no rules are informed, " + "defaults to allowing all account permissions. Otherwise, only the explicitly informed permissions for the key pair will be " + "considered. Lower indexed rules take precedence over higher. Thus, in the following example: " + "\"rules[0].rule=deleteUserKeys rules[0].permission=deny rules[1].rule=*UserKey* rules[1].permission=allow\", all rules matching " + - "the expression \"*UserKeys*\" will be allowed, except for \"deleteUserKeys\".") + "the expression \"*UserKeys*\" will be allowed, except for \"deleteUserKeys\".", since = "4.23.0") private Map rules; public void setUserId(Long userId) { From faabcc07e54576f6f1cafca17e62357838a6bde7 Mon Sep 17 00:00:00 2001 From: Fabricio Duarte Date: Mon, 17 Aug 2026 13:43:06 -0300 Subject: [PATCH 3/3] Remove unused labels --- ui/public/locales/en.json | 2 -- ui/public/locales/pt_BR.json | 4 +--- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json index 775de26103a0..f57460efa482 100644 --- a/ui/public/locales/en.json +++ b/ui/public/locales/en.json @@ -1731,7 +1731,6 @@ "message.memory.usage.info.hypervisor.additionals": "The data shown may not reflect the actual memory usage if the Instance does not have the additional hypervisor tools installed", "message.memory.usage.info.negative.value": "If the Instance's memory usage cannot be obtained from the hypervisor, the lines for free memory in the raw data graph and memory usage in the percentage graph will be disabled", "message.migrate.volume.tooltip": "Volume can be migrated to any suitable storage pool. Admin has to choose the appropriate disk offering to replace, that supports the new storage pool", -"message.register.keypair.failed": "Failed to register API key pair", "label.migrate.with.storage": "Migrate with storage", "label.migrating": "Migrating", "label.migrating.data": "Migrating data", @@ -2200,7 +2199,6 @@ "label.register.user.data": "Register User Data", "label.register.cni.config": "Register CNI Configuration", "label.register.user.data.details": "Enter the User Data in plain text or in Base64 encoding. Up to 32KB of Base64 encoded User Data can be sent by default. The setting vm.userdata.max.length can be used to increase the limit to upto 1MB.", -"label.registering.keypair": "Registering API key pair for user \"{user}\"", "label.reinstall.vm": "Reinstall Instance", "label.reject": "Reject", "label.related": "Related", diff --git a/ui/public/locales/pt_BR.json b/ui/public/locales/pt_BR.json index b3eae6eb11ce..8fba779b1d12 100644 --- a/ui/public/locales/pt_BR.json +++ b/ui/public/locales/pt_BR.json @@ -51,7 +51,7 @@ "label.action": "A\u00e7\u00e3o", "label.action.attach.disk": "Anexar disco", "label.action.attach.iso": "Anexar ISO", -"label.action.bulk.delete.api.keys": "Apagar em massa as chaves de acesso \u00e0 API.", +"label.action.bulk.delete.api.keys": "Apagar em massa as chaves de acesso \u00e0 API", "label.action.bulk.delete.egress.firewall.rules": "Apagar em massa as regras de sa\u00edda do firewall.", "label.action.bulk.delete.firewall.rules": "Apagar em massa as regras do firewall.", "label.action.bulk.delete.ip.v6.firewall.rules": "Apagar em massa as regras de firewall IPv6.", @@ -1931,7 +1931,6 @@ "label.register.oauth": "Registrar OAuth", "label.register.user.data": "Registrar dados de usu\u00e1rio", "label.register.template": "Registrar template", -"label.registering.keypair": "Registrando par de chaves de API para o usu\u00e1rio \"{user}\"", "label.reinstall.vm": "Reinstalar VM", "label.reject": "Rejeitar", "label.related": "Relacionado", @@ -3445,7 +3444,6 @@ "message.read.accept.license.agreements": "Leia e aceite os termos dos contratos de licen\u00e7a.", "message.read.admin.guide.scaling.up": "Por favor leia a sess\u00e3o sobre escalonamento din\u00e2mico no guia do administrador antes de escalonar.", "message.recover.vm": "Por favor, confirme a recupera\u00e7\u00e3o desta VM.", -"message.register.keypair.failed": "Falha ao registrar par de chave de API", "message.reinstall.vm": "NOTA: proceda com cuidado. Isso far\u00e1 com que a m\u00e1quina virtual seja re-instalada a partir do template. Todos os dados do disco root ser\u00e3o perdidos. Se houver volumes de dados adicionais, eles n\u00e3o ser\u00e3o alterados.", "message.release.ip.failed": "Falha ao liberar IP", "message.releasing.dedicated.cluster": "Liberando cluster dedicado...",