diff --git a/api/src/main/java/com/cloud/event/EventTypes.java b/api/src/main/java/com/cloud/event/EventTypes.java index 5e5309965c1e..81ed185dae5a 100644 --- a/api/src/main/java/com/cloud/event/EventTypes.java +++ b/api/src/main/java/com/cloud/event/EventTypes.java @@ -292,6 +292,7 @@ public class EventTypes { //register for user API and secret keys public static final String EVENT_REGISTER_FOR_SECRET_API_KEY = "REGISTER.USER.KEY"; + public static final String API_KEY_ACCESS_UPDATE = "API.KEY.ACCESS.UPDATE"; // Template Events public static final String EVENT_TEMPLATE_CREATE = "TEMPLATE.CREATE"; diff --git a/api/src/main/java/com/cloud/user/Account.java b/api/src/main/java/com/cloud/user/Account.java index bb9838f137a9..6be4d0a48f6e 100644 --- a/api/src/main/java/com/cloud/user/Account.java +++ b/api/src/main/java/com/cloud/user/Account.java @@ -93,4 +93,8 @@ public static Type getFromValue(Integer type){ boolean isDefault(); + public void setApiKeyAccess(Boolean apiKeyAccess); + + public Boolean getApiKeyAccess(); + } diff --git a/api/src/main/java/com/cloud/user/AccountService.java b/api/src/main/java/com/cloud/user/AccountService.java index 60db7abb7346..e2c3bed0c295 100644 --- a/api/src/main/java/com/cloud/user/AccountService.java +++ b/api/src/main/java/com/cloud/user/AccountService.java @@ -19,6 +19,7 @@ import java.util.List; import java.util.Map; +import com.cloud.utils.Pair; import org.apache.cloudstack.acl.ControlledEntity; import org.apache.cloudstack.acl.RoleType; import org.apache.cloudstack.acl.SecurityChecker.AccessType; @@ -127,9 +128,9 @@ User createUser(String userName, String password, String firstName, String lastN */ UserAccount getUserAccountById(Long userId); - public Map getKeys(GetUserKeysCmd cmd); + public Pair> getKeys(GetUserKeysCmd cmd); - public Map getKeys(Long userId); + public Pair> getKeys(Long userId); /** * Lists user two-factor authentication provider plugins diff --git a/api/src/main/java/com/cloud/user/User.java b/api/src/main/java/com/cloud/user/User.java index 422e264f10be..041b39ad2729 100644 --- a/api/src/main/java/com/cloud/user/User.java +++ b/api/src/main/java/com/cloud/user/User.java @@ -94,4 +94,9 @@ public enum Source { public boolean isUser2faEnabled(); public String getKeyFor2fa(); + + public void setApiKeyAccess(Boolean apiKeyAccess); + + public Boolean getApiKeyAccess(); + } diff --git a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java index a6c6991be242..8f78fe5c4b4c 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -35,6 +35,7 @@ public class ApiConstants { public static final String ALLOW_USER_FORCE_STOP_VM = "allowuserforcestopvm"; public static final String ANNOTATION = "annotation"; public static final String API_KEY = "apikey"; + public static final String API_KEY_ACCESS = "apikeyaccess"; public static final String ARCHIVED = "archived"; public static final String ARCH = "arch"; public static final String AS_NUMBER = "asnumber"; @@ -1247,4 +1248,30 @@ public enum VMDetails { public enum DomainDetails { all, resource, min; } + + public enum ApiKeyAccess { + DISABLED(false), + ENABLED(true), + INHERIT(null); + + Boolean apiKeyAccess; + + ApiKeyAccess(Boolean keyAccess) { + apiKeyAccess = keyAccess; + } + + public Boolean toBoolean() { + return apiKeyAccess; + } + + public static ApiKeyAccess fromBoolean(Boolean value) { + if (value == null) { + return INHERIT; + } else if (value) { + return ENABLED; + } else { + return DISABLED; + } + } + } } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/account/UpdateAccountCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/account/UpdateAccountCmd.java index 91cbb90e4da4..3347a0d09f37 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/account/UpdateAccountCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/account/UpdateAccountCmd.java @@ -21,7 +21,9 @@ import javax.inject.Inject; +import org.apache.cloudstack.acl.RoleType; import org.apache.cloudstack.api.ApiCommandResourceType; +import org.apache.cloudstack.api.command.user.UserCmd; import org.apache.cloudstack.api.response.RoleResponse; import org.apache.cloudstack.acl.SecurityChecker.AccessType; @@ -40,8 +42,8 @@ import com.cloud.user.Account; @APICommand(name = "updateAccount", description = "Updates account information for the authenticated user", responseObject = AccountResponse.class, entityType = {Account.class}, - requestHasSensitiveInfo = false, responseHasSensitiveInfo = true) -public class UpdateAccountCmd extends BaseCmd { + responseView = ResponseView.Restricted, requestHasSensitiveInfo = false, responseHasSensitiveInfo = true) +public class UpdateAccountCmd extends BaseCmd implements UserCmd { ///////////////////////////////////////////////////// //////////////// API parameters ///////////////////// @@ -70,6 +72,9 @@ public class UpdateAccountCmd extends BaseCmd { @Parameter(name = ApiConstants.ACCOUNT_DETAILS, type = CommandType.MAP, description = "Details for the account used to store specific parameters") private Map details; + @Parameter(name = ApiConstants.API_KEY_ACCESS, type = CommandType.STRING, description = "Determines if Api key access for this user is enabled, disabled or inherits the value from its parent, the domain level setting api.key.access", since = "4.20.1.0", authorized = {RoleType.Admin}) + private String apiKeyAccess; + @Inject RegionService _regionService; @@ -109,6 +114,10 @@ public Map getDetails() { return params; } + public String getApiKeyAccess() { + return apiKeyAccess; + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// @@ -131,7 +140,7 @@ public long getEntityOwnerId() { public void execute() { Account result = _regionService.updateAccount(this); if (result != null){ - AccountResponse response = _responseGenerator.createAccountResponse(ResponseView.Full, result); + AccountResponse response = _responseGenerator.createAccountResponse(getResponseView(), result); response.setResponseName(getCommandName()); setResponseObject(response); } else { diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/user/GetUserKeysCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/user/GetUserKeysCmd.java index 3a3414d95d8a..cdd239f72b57 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/user/GetUserKeysCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/user/GetUserKeysCmd.java @@ -20,6 +20,7 @@ import com.cloud.user.Account; import com.cloud.user.User; +import com.cloud.utils.Pair; import org.apache.cloudstack.acl.RoleType; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiConstants; @@ -54,11 +55,13 @@ public Long getID(){ else return Account.ACCOUNT_ID_SYSTEM; } public void execute(){ - Map keys = _accountService.getKeys(this); + Pair> keys = _accountService.getKeys(this); + RegisterResponse response = new RegisterResponse(); if(keys != null){ - response.setApiKey(keys.get("apikey")); - response.setSecretKey(keys.get("secretkey")); + response.setApiKeyAccess(keys.first()); + response.setApiKey(keys.second().get("apikey")); + response.setSecretKey(keys.second().get("secretkey")); } response.setObjectName("userkeys"); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/user/ListUsersCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/user/ListUsersCmd.java index ef9e3fa22405..27a78c738c9a 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/user/ListUsersCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/user/ListUsersCmd.java @@ -19,20 +19,23 @@ import com.cloud.server.ResourceIcon; import com.cloud.server.ResourceTag; import com.cloud.user.Account; +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.command.user.UserCmd; import org.apache.cloudstack.api.response.ResourceIconResponse; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.BaseListAccountResourcesCmd; import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ResponseObject.ResponseView; import org.apache.cloudstack.api.response.ListResponse; import org.apache.cloudstack.api.response.UserResponse; import java.util.List; @APICommand(name = "listUsers", description = "Lists user accounts", responseObject = UserResponse.class, - requestHasSensitiveInfo = false, responseHasSensitiveInfo = true) -public class ListUsersCmd extends BaseListAccountResourcesCmd { + responseView = ResponseView.Restricted, requestHasSensitiveInfo = false, responseHasSensitiveInfo = true) +public class ListUsersCmd extends BaseListAccountResourcesCmd implements UserCmd { ///////////////////////////////////////////////////// @@ -53,6 +56,9 @@ public class ListUsersCmd extends BaseListAccountResourcesCmd { @Parameter(name = ApiConstants.USERNAME, type = CommandType.STRING, description = "List user by the username") private String username; + @Parameter(name = ApiConstants.API_KEY_ACCESS, type = CommandType.STRING, description = "List users by the Api key access value", since = "4.20.1.0", authorized = {RoleType.Admin}) + private String apiKeyAccess; + @Parameter(name = ApiConstants.SHOW_RESOURCE_ICON, type = CommandType.BOOLEAN, description = "flag to display the resource icon for users") private Boolean showIcon; @@ -77,6 +83,10 @@ public String getUsername() { return username; } + public String getApiKeyAccess() { + return apiKeyAccess; + } + public Boolean getShowIcon() { return showIcon != null ? showIcon : false; } @@ -87,7 +97,7 @@ public Boolean getShowIcon() { @Override public void execute() { - ListResponse response = _queryService.searchForUsers(this); + ListResponse response = _queryService.searchForUsers(getResponseView(), this); response.setResponseName(getCommandName()); this.setResponseObject(response); if (response != null && response.getCount() > 0 && getShowIcon()) { diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/user/UpdateUserCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/user/UpdateUserCmd.java index c9e1e934152d..3d7f51ae2204 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/user/UpdateUserCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/user/UpdateUserCmd.java @@ -18,6 +18,7 @@ import javax.inject.Inject; +import org.apache.cloudstack.acl.RoleType; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiCommandResourceType; import org.apache.cloudstack.api.ApiConstants; @@ -69,6 +70,9 @@ public class UpdateUserCmd extends BaseCmd { @Parameter(name = ApiConstants.USER_SECRET_KEY, type = CommandType.STRING, description = "The secret key for the user. Must be specified with userApiKey") private String secretKey; + @Parameter(name = ApiConstants.API_KEY_ACCESS, type = CommandType.STRING, description = "Determines if Api key access for this user is enabled, disabled or inherits the value from its parent, the owning account", since = "4.20.1.0", authorized = {RoleType.Admin}) + private String apiKeyAccess; + @Parameter(name = ApiConstants.TIMEZONE, type = CommandType.STRING, description = "Specifies a timezone for this command. For more information on the timezone parameter, see Time Zone Format.") @@ -120,6 +124,10 @@ public String getSecretKey() { return secretKey; } + public String getApiKeyAccess() { + return apiKeyAccess; + } + public String getTimezone() { return timezone; } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/account/ListAccountsCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/account/ListAccountsCmd.java index 0a962b19e4f3..9157188fdeee 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/account/ListAccountsCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/account/ListAccountsCmd.java @@ -20,6 +20,7 @@ import java.util.EnumSet; import java.util.List; +import org.apache.cloudstack.acl.RoleType; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiCommandResourceType; import org.apache.cloudstack.api.ApiConstants; @@ -70,6 +71,9 @@ public class ListAccountsCmd extends BaseListDomainResourcesCmd implements UserC description = "comma separated list of account details requested, value can be a list of [ all, resource, min]") private List viewDetails; + @Parameter(name = ApiConstants.API_KEY_ACCESS, type = CommandType.STRING, description = "List accounts by the Api key access value", since = "4.20.1.0", authorized = {RoleType.Admin}) + private String apiKeyAccess; + @Parameter(name = ApiConstants.SHOW_RESOURCE_ICON, type = CommandType.BOOLEAN, description = "flag to display the resource icon for accounts") private Boolean showIcon; @@ -120,6 +124,10 @@ public EnumSet getDetails() throws InvalidParameterValueException return dv; } + public String getApiKeyAccess() { + return apiKeyAccess; + } + public boolean getShowIcon() { return showIcon != null ? showIcon : false; } diff --git a/api/src/main/java/org/apache/cloudstack/api/response/AccountResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/AccountResponse.java index 7a84e85a4a6f..6fc098295f64 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/AccountResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/AccountResponse.java @@ -271,6 +271,10 @@ public class AccountResponse extends BaseResponse implements ResourceLimitAndCou @Param(description = "The tagged resource limit and count for the account", since = "4.20.0") List taggedResources; + @SerializedName(ApiConstants.API_KEY_ACCESS) + @Param(description = "whether api key access is Enabled, Disabled or set to Inherit (it inherits the value from the parent)", since = "4.20.1.0") + ApiConstants.ApiKeyAccess apiKeyAccess; + @Override public String getObjectId() { return id; @@ -554,4 +558,8 @@ public void setResourceIconResponse(ResourceIconResponse icon) { public void setTaggedResourceLimitsAndCounts(List taggedResourceLimitsAndCounts) { this.taggedResources = taggedResourceLimitsAndCounts; } + + public void setApiKeyAccess(Boolean apiKeyAccess) { + this.apiKeyAccess = ApiConstants.ApiKeyAccess.fromBoolean(apiKeyAccess); + } } diff --git a/api/src/main/java/org/apache/cloudstack/api/response/RegisterResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/RegisterResponse.java index 5faedabfc16b..dd17cc5cc8af 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/RegisterResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/RegisterResponse.java @@ -18,19 +18,24 @@ import com.google.gson.annotations.SerializedName; +import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.BaseResponse; import com.cloud.serializer.Param; public class RegisterResponse extends BaseResponse { - @SerializedName("apikey") + @SerializedName(ApiConstants.API_KEY) @Param(description = "the api key of the registered user", isSensitive = true) private String apiKey; - @SerializedName("secretkey") + @SerializedName(ApiConstants.SECRET_KEY) @Param(description = "the secret key of the registered user", isSensitive = true) private String secretKey; + @SerializedName(ApiConstants.API_KEY_ACCESS) + @Param(description = "whether api key access is allowed or not", isSensitive = true) + private Boolean apiKeyAccess; + public String getApiKey() { return apiKey; } @@ -46,4 +51,8 @@ public String getSecretKey() { public void setSecretKey(String secretKey) { this.secretKey = secretKey; } + + public void setApiKeyAccess(Boolean apiKeyAccess) { + this.apiKeyAccess = apiKeyAccess; + } } diff --git a/api/src/main/java/org/apache/cloudstack/api/response/UserResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/UserResponse.java index 1a17f3b86988..df97a915700f 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/UserResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/UserResponse.java @@ -128,6 +128,10 @@ public class UserResponse extends BaseResponse implements SetResourceIconRespons @Param(description = "true if user has two factor authentication is mandated", since = "4.18.0.0") private Boolean is2FAmandated; + @SerializedName(ApiConstants.API_KEY_ACCESS) + @Param(description = "whether api key access is Enabled, Disabled or set to Inherit (it inherits the value from the parent)", since = "4.20.1.0") + ApiConstants.ApiKeyAccess apiKeyAccess; + @Override public String getObjectId() { return this.getId(); @@ -309,4 +313,8 @@ public Boolean getIs2FAmandated() { public void set2FAmandated(Boolean is2FAmandated) { this.is2FAmandated = is2FAmandated; } + + public void setApiKeyAccess(Boolean apiKeyAccess) { + this.apiKeyAccess = ApiConstants.ApiKeyAccess.fromBoolean(apiKeyAccess); + } } diff --git a/api/src/main/java/org/apache/cloudstack/query/QueryService.java b/api/src/main/java/org/apache/cloudstack/query/QueryService.java index c93e43d9f371..88081494320c 100644 --- a/api/src/main/java/org/apache/cloudstack/query/QueryService.java +++ b/api/src/main/java/org/apache/cloudstack/query/QueryService.java @@ -19,6 +19,7 @@ import java.util.List; import org.apache.cloudstack.affinity.AffinityGroupResponse; +import org.apache.cloudstack.api.ResponseObject; import org.apache.cloudstack.api.command.admin.domain.ListDomainsCmd; import org.apache.cloudstack.api.command.admin.host.ListHostTagsCmd; import org.apache.cloudstack.api.command.admin.host.ListHostsCmd; @@ -130,7 +131,7 @@ public interface QueryService { ConfigKey ReturnVmStatsOnVmList = new ConfigKey<>("Advanced", Boolean.class, "list.vm.default.details.stats", "true", "Determines whether VM stats should be returned when details are not explicitly specified in listVirtualMachines API request. When false, details default to [group, nics, secgrp, tmpl, servoff, diskoff, backoff, iso, volume, min, affgrp]. When true, all details are returned including 'stats'.", true, ConfigKey.Scope.Global); - ListResponse searchForUsers(ListUsersCmd cmd) throws PermissionDeniedException; + ListResponse searchForUsers(ResponseObject.ResponseView responseView, ListUsersCmd cmd) throws PermissionDeniedException; ListResponse searchForUsers(Long domainId, boolean recursive) throws PermissionDeniedException; diff --git a/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java b/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java index cb2190073254..abf860439375 100644 --- a/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java +++ b/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java @@ -88,6 +88,7 @@ import com.cloud.upgrade.dao.Upgrade41810to41900; import com.cloud.upgrade.dao.Upgrade41900to41910; import com.cloud.upgrade.dao.Upgrade41910to42000; +import com.cloud.upgrade.dao.Upgrade42000to42010; import com.cloud.upgrade.dao.Upgrade420to421; import com.cloud.upgrade.dao.Upgrade421to430; import com.cloud.upgrade.dao.Upgrade430to440; @@ -230,6 +231,7 @@ public DatabaseUpgradeChecker() { .next("4.18.1.0", new Upgrade41810to41900()) .next("4.19.0.0", new Upgrade41900to41910()) .next("4.19.1.0", new Upgrade41910to42000()) + .next("4.20.0.0", new Upgrade42000to42010()) .build(); } diff --git a/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42000to42010.java b/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42000to42010.java new file mode 100644 index 000000000000..197ca1cb34c7 --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42000to42010.java @@ -0,0 +1,83 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package com.cloud.upgrade.dao; + +import java.io.InputStream; +import java.sql.Connection; + +import com.cloud.upgrade.SystemVmTemplateRegistration; +import com.cloud.utils.exception.CloudRuntimeException; + +public class Upgrade42000to42010 extends DbUpgradeAbstractImpl implements DbUpgrade, DbUpgradeSystemVmTemplate { + private SystemVmTemplateRegistration systemVmTemplateRegistration; + + @Override + public String[] getUpgradableVersionRange() { + return new String[] {"4.20.0.0", "4.20.1.0"}; + } + + @Override + public String getUpgradedVersion() { + return "4.20.1.0"; + } + + @Override + public boolean supportsRollingUpgrade() { + return false; + } + + @Override + public InputStream[] getPrepareScripts() { + final String scriptFile = "META-INF/db/schema-42000to42010.sql"; + final InputStream script = Thread.currentThread().getContextClassLoader().getResourceAsStream(scriptFile); + if (script == null) { + throw new CloudRuntimeException("Unable to find " + scriptFile); + } + + return new InputStream[] {script}; + } + + @Override + public void performDataMigration(Connection conn) { + } + + @Override + public InputStream[] getCleanupScripts() { + final String scriptFile = "META-INF/db/schema-42000to42010-cleanup.sql"; + final InputStream script = Thread.currentThread().getContextClassLoader().getResourceAsStream(scriptFile); + if (script == null) { + throw new CloudRuntimeException("Unable to find " + scriptFile); + } + + return new InputStream[] {script}; + } + + private void initSystemVmTemplateRegistration() { + systemVmTemplateRegistration = new SystemVmTemplateRegistration(""); + } + + @Override + public void updateSystemVmTemplates(Connection conn) { + logger.debug("Updating System Vm template IDs"); + initSystemVmTemplateRegistration(); + try { + systemVmTemplateRegistration.updateSystemVmTemplates(conn); + } catch (Exception e) { + throw new CloudRuntimeException("Failed to find / register SystemVM template(s)"); + } + } +} diff --git a/engine/schema/src/main/java/com/cloud/user/AccountVO.java b/engine/schema/src/main/java/com/cloud/user/AccountVO.java index f04b2bafbde6..74a538565d77 100644 --- a/engine/schema/src/main/java/com/cloud/user/AccountVO.java +++ b/engine/schema/src/main/java/com/cloud/user/AccountVO.java @@ -77,6 +77,9 @@ public class AccountVO implements Account { @Column(name = "default") boolean isDefault; + @Column(name = "api_key_access") + private Boolean apiKeyAccess; + public AccountVO() { uuid = UUID.randomUUID().toString(); } @@ -229,4 +232,14 @@ public String getName() { public String reflectionToString() { return ReflectionToStringBuilderUtils.reflectOnlySelectedFields(this, "id", "uuid", "accountName", "domainId"); } + + @Override + public void setApiKeyAccess(Boolean apiKeyAccess) { + this.apiKeyAccess = apiKeyAccess; + } + + @Override + public Boolean getApiKeyAccess() { + return apiKeyAccess; + } } diff --git a/engine/schema/src/main/java/com/cloud/user/UserVO.java b/engine/schema/src/main/java/com/cloud/user/UserVO.java index 69970bf2d2cd..7dac26429ace 100644 --- a/engine/schema/src/main/java/com/cloud/user/UserVO.java +++ b/engine/schema/src/main/java/com/cloud/user/UserVO.java @@ -115,6 +115,9 @@ public class UserVO implements User, Identity, InternalIdentity { @Column(name = "key_for_2fa") private String keyFor2fa; + @Column(name = "api_key_access") + private Boolean apiKeyAccess; + public UserVO() { this.uuid = UUID.randomUUID().toString(); } @@ -350,4 +353,13 @@ public void setUser2faProvider(String user2faProvider) { this.user2faProvider = user2faProvider; } + @Override + public void setApiKeyAccess(Boolean apiKeyAccess) { + this.apiKeyAccess = apiKeyAccess; + } + + @Override + public Boolean getApiKeyAccess() { + return apiKeyAccess; + } } diff --git a/engine/schema/src/main/java/com/cloud/user/dao/AccountDaoImpl.java b/engine/schema/src/main/java/com/cloud/user/dao/AccountDaoImpl.java index eed5572a0b24..f9ef5c40eba2 100644 --- a/engine/schema/src/main/java/com/cloud/user/dao/AccountDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/user/dao/AccountDaoImpl.java @@ -41,8 +41,8 @@ @Component public class AccountDaoImpl extends GenericDaoBase implements AccountDao { - private static final String FIND_USER_ACCOUNT_BY_API_KEY = "SELECT u.id, u.username, u.account_id, u.secret_key, u.state, " - + "a.id, a.account_name, a.type, a.role_id, a.domain_id, a.state " + "FROM `cloud`.`user` u, `cloud`.`account` a " + private static final String FIND_USER_ACCOUNT_BY_API_KEY = "SELECT u.id, u.username, u.account_id, u.secret_key, u.state, u.api_key_access, " + + "a.id, a.account_name, a.type, a.role_id, a.domain_id, a.state, a.api_key_access " + "FROM `cloud`.`user` u, `cloud`.`account` a " + "WHERE u.account_id = a.id AND u.api_key = ? and u.removed IS NULL"; protected final SearchBuilder AllFieldsSearch; @@ -148,13 +148,25 @@ public Pair findUserAccountByApiKey(String apiKey) { u.setAccountId(rs.getLong(3)); u.setSecretKey(DBEncryptionUtil.decrypt(rs.getString(4))); u.setState(State.getValueOf(rs.getString(5))); - - AccountVO a = new AccountVO(rs.getLong(6)); - a.setAccountName(rs.getString(7)); - a.setType(Account.Type.getFromValue(rs.getInt(8))); - a.setRoleId(rs.getLong(9)); - a.setDomainId(rs.getLong(10)); - a.setState(State.getValueOf(rs.getString(11))); + boolean apiKeyAccess = rs.getBoolean(6); + if (rs.wasNull()) { + u.setApiKeyAccess(null); + } else { + u.setApiKeyAccess(apiKeyAccess); + } + + AccountVO a = new AccountVO(rs.getLong(7)); + a.setAccountName(rs.getString(8)); + a.setType(Account.Type.getFromValue(rs.getInt(9))); + a.setRoleId(rs.getLong(10)); + a.setDomainId(rs.getLong(11)); + a.setState(State.getValueOf(rs.getString(12))); + apiKeyAccess = rs.getBoolean(13); + if (rs.wasNull()) { + a.setApiKeyAccess(null); + } else { + a.setApiKeyAccess(apiKeyAccess); + } userAcctPair = new Pair(u, a); } diff --git a/engine/schema/src/main/resources/META-INF/db/schema-42000to42010-cleanup.sql b/engine/schema/src/main/resources/META-INF/db/schema-42000to42010-cleanup.sql new file mode 100644 index 000000000000..d187b6fa0439 --- /dev/null +++ b/engine/schema/src/main/resources/META-INF/db/schema-42000to42010-cleanup.sql @@ -0,0 +1,20 @@ +-- Licensed to the Apache Software Foundation (ASF) under one +-- or more contributor license agreements. See the NOTICE file +-- distributed with this work for additional information +-- regarding copyright ownership. The ASF licenses this file +-- to you under the Apache License, Version 2.0 (the +-- "License"); you may not use this file except in compliance +-- with the License. You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, +-- software distributed under the License is distributed on an +-- "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +-- KIND, either express or implied. See the License for the +-- specific language governing permissions and limitations +-- under the License. + +--; +-- Schema upgrade cleanup from 4.20.0.0 to 4.20.1.0 +--; diff --git a/engine/schema/src/main/resources/META-INF/db/schema-42000to42010.sql b/engine/schema/src/main/resources/META-INF/db/schema-42000to42010.sql new file mode 100644 index 000000000000..31c4928d81bf --- /dev/null +++ b/engine/schema/src/main/resources/META-INF/db/schema-42000to42010.sql @@ -0,0 +1,24 @@ +-- Licensed to the Apache Software Foundation (ASF) under one +-- or more contributor license agreements. See the NOTICE file +-- distributed with this work for additional information +-- regarding copyright ownership. The ASF licenses this file +-- to you under the Apache License, Version 2.0 (the +-- "License"); you may not use this file except in compliance +-- with the License. You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, +-- software distributed under the License is distributed on an +-- "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +-- KIND, either express or implied. See the License for the +-- specific language governing permissions and limitations +-- under the License. + +--; +-- Schema upgrade from 4.20.0.0 to 4.20.1.0 +--; + +-- Add column api_key_access to user and account tables +CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.user', 'api_key_access', 'boolean DEFAULT NULL COMMENT "is api key access allowed for the user" AFTER `secret_key`'); +CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.account', 'api_key_access', 'boolean DEFAULT NULL COMMENT "is api key access allowed for the account" '); diff --git a/engine/schema/src/main/resources/META-INF/db/views/cloud.account_view.sql b/engine/schema/src/main/resources/META-INF/db/views/cloud.account_view.sql index 87546a9d1188..dc64380fb57b 100644 --- a/engine/schema/src/main/resources/META-INF/db/views/cloud.account_view.sql +++ b/engine/schema/src/main/resources/META-INF/db/views/cloud.account_view.sql @@ -31,6 +31,7 @@ select `account`.`cleanup_needed` AS `cleanup_needed`, `account`.`network_domain` AS `network_domain` , `account`.`default` AS `default`, + `account`.`api_key_access` AS `api_key_access`, `domain`.`id` AS `domain_id`, `domain`.`uuid` AS `domain_uuid`, `domain`.`name` AS `domain_name`, diff --git a/engine/schema/src/main/resources/META-INF/db/views/cloud.user_view.sql b/engine/schema/src/main/resources/META-INF/db/views/cloud.user_view.sql index 7eedc03712b6..340cfa9055fb 100644 --- a/engine/schema/src/main/resources/META-INF/db/views/cloud.user_view.sql +++ b/engine/schema/src/main/resources/META-INF/db/views/cloud.user_view.sql @@ -39,6 +39,7 @@ select user.incorrect_login_attempts, user.source, user.default, + user.api_key_access, account.id account_id, account.uuid account_uuid, account.account_name account_name, diff --git a/framework/config/src/main/java/org/apache/cloudstack/framework/config/ConfigKey.java b/framework/config/src/main/java/org/apache/cloudstack/framework/config/ConfigKey.java index 36a8050754c0..00cf56345c8d 100644 --- a/framework/config/src/main/java/org/apache/cloudstack/framework/config/ConfigKey.java +++ b/framework/config/src/main/java/org/apache/cloudstack/framework/config/ConfigKey.java @@ -34,6 +34,7 @@ public class ConfigKey { public static final String CATEGORY_ADVANCED = "Advanced"; public static final String CATEGORY_ALERT = "Alert"; public static final String CATEGORY_NETWORK = "Network"; + public static final String CATEGORY_SYSTEM = "System"; public enum Scope { Global, Zone, Cluster, StoragePool, Account, ManagementServer, ImageStore, Domain diff --git a/plugins/network-elements/juniper-contrail/src/test/java/org/apache/cloudstack/network/contrail/management/MockAccountManager.java b/plugins/network-elements/juniper-contrail/src/test/java/org/apache/cloudstack/network/contrail/management/MockAccountManager.java index 3a5541654bbe..7d27e6b77cec 100644 --- a/plugins/network-elements/juniper-contrail/src/test/java/org/apache/cloudstack/network/contrail/management/MockAccountManager.java +++ b/plugins/network-elements/juniper-contrail/src/test/java/org/apache/cloudstack/network/contrail/management/MockAccountManager.java @@ -486,12 +486,12 @@ public void checkAccess(Account account, VpcOffering vof, DataCenter zone) throw } @Override - public Map getKeys(GetUserKeysCmd cmd){ + public Pair> getKeys(GetUserKeysCmd cmd){ return null; } @Override - public Map getKeys(Long userId) { + public Pair> getKeys(Long userId) { return null; } diff --git a/server/src/main/java/com/cloud/api/ApiDBUtils.java b/server/src/main/java/com/cloud/api/ApiDBUtils.java index a169ebc0f19f..944f60d292ca 100644 --- a/server/src/main/java/com/cloud/api/ApiDBUtils.java +++ b/server/src/main/java/com/cloud/api/ApiDBUtils.java @@ -1945,11 +1945,11 @@ public static InstanceGroupJoinVO newInstanceGroupView(InstanceGroup e) { } public static UserResponse newUserResponse(UserAccountJoinVO usr) { - return newUserResponse(usr, null); + return newUserResponse(ResponseView.Restricted, null, usr); } - public static UserResponse newUserResponse(UserAccountJoinVO usr, Long domainId) { - UserResponse response = s_userAccountJoinDao.newUserResponse(usr); + public static UserResponse newUserResponse(ResponseView view, Long domainId, UserAccountJoinVO usr) { + UserResponse response = s_userAccountJoinDao.newUserResponse(view, usr); if(!AccountManager.UseSecretKeyInResponse.value()){ response.setSecretKey(null); } diff --git a/server/src/main/java/com/cloud/api/ApiServer.java b/server/src/main/java/com/cloud/api/ApiServer.java index 72e97c3a6eeb..98f87dfc3f08 100644 --- a/server/src/main/java/com/cloud/api/ApiServer.java +++ b/server/src/main/java/com/cloud/api/ApiServer.java @@ -188,6 +188,7 @@ import com.cloud.utils.net.NetUtils; import com.google.gson.reflect.TypeToken; +import static com.cloud.user.AccountManagerImpl.apiKeyAccess; import static org.apache.cloudstack.user.UserPasswordResetManager.UserPasswordResetEnabled; @Component @@ -896,6 +897,34 @@ private void buildAuditTrail(final StringBuilder auditTrailSb, final String comm } } + protected boolean verifyApiKeyAccessAllowed(User user, Account account) { + Boolean apiKeyAccessEnabled = user.getApiKeyAccess(); + if (apiKeyAccessEnabled != null) { + if (Boolean.TRUE.equals(apiKeyAccessEnabled)) { + return true; + } else { + logger.info("Api-Key access is disabled for the User " + user.toString()); + return false; + } + } + apiKeyAccessEnabled = account.getApiKeyAccess(); + if (apiKeyAccessEnabled != null) { + if (Boolean.TRUE.equals(apiKeyAccessEnabled)) { + return true; + } else { + logger.info("Api-Key access is disabled for the Account " + account.toString()); + return false; + } + } + apiKeyAccessEnabled = apiKeyAccess.valueIn(account.getDomainId()); + if (Boolean.TRUE.equals(apiKeyAccessEnabled)) { + return true; + } else { + logger.info("Api-Key access is disabled by the Domain level setting api.key.access"); + } + return false; + } + @Override public boolean verifyRequest(final Map requestParameters, final Long userId, InetAddress remoteAddress) throws ServerApiException { try { @@ -1012,6 +1041,10 @@ public boolean verifyRequest(final Map requestParameters, fina return false; } + if (!verifyApiKeyAccessAllowed(user, account)) { + return false; + } + if (!commandAvailable(remoteAddress, commandName, user)) { return false; } diff --git a/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java b/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java index 25018bc2c369..976d3817a0a4 100644 --- a/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java +++ b/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java @@ -661,10 +661,13 @@ private SearchCriteria getMinimumCpuSpeedServiceOfferingJ * .api.command.admin.user.ListUsersCmd) */ @Override - public ListResponse searchForUsers(ListUsersCmd cmd) throws PermissionDeniedException { + public ListResponse searchForUsers(ResponseView responseView, ListUsersCmd cmd) throws PermissionDeniedException { Pair, Integer> result = searchForUsersInternal(cmd); ListResponse response = new ListResponse(); - List userResponses = ViewResponseHelper.createUserResponse(CallContext.current().getCallingAccount().getDomainId(), + if (CallContext.current().getCallingAccount().getType() == Account.Type.ADMIN) { + responseView = ResponseView.Full; + } + List userResponses = ViewResponseHelper.createUserResponse(responseView, CallContext.current().getCallingAccount().getDomainId(), result.first().toArray(new UserAccountJoinVO[result.first().size()])); response.setResponses(userResponses, result.second()); return response; @@ -691,10 +694,10 @@ public ListResponse searchForUsers(Long domainId, boolean recursiv Object state = null; String keyword = null; - Pair, Integer> result = getUserListInternal(caller, permittedAccounts, listAll, id, username, type, accountName, state, keyword, domainId, recursive, - null); + Pair, Integer> result = getUserListInternal(caller, permittedAccounts, listAll, id, + username, type, accountName, state, keyword, null, domainId, recursive, null); ListResponse response = new ListResponse(); - List userResponses = ViewResponseHelper.createUserResponse(CallContext.current().getCallingAccount().getDomainId(), + List userResponses = ViewResponseHelper.createUserResponse(ResponseView.Restricted, CallContext.current().getCallingAccount().getDomainId(), result.first().toArray(new UserAccountJoinVO[result.first().size()])); response.setResponses(userResponses, result.second()); return response; @@ -719,6 +722,7 @@ private Pair, Integer> searchForUsersInternal(ListUsersC String accountName = cmd.getAccountName(); Object state = cmd.getState(); String keyword = cmd.getKeyword(); + String apiKeyAccess = cmd.getApiKeyAccess(); Long domainId = cmd.getDomainId(); boolean recursive = cmd.isRecursive(); @@ -727,11 +731,11 @@ private Pair, Integer> searchForUsersInternal(ListUsersC Filter searchFilter = new Filter(UserAccountJoinVO.class, "id", true, startIndex, pageSizeVal); - return getUserListInternal(caller, permittedAccounts, listAll, id, username, type, accountName, state, keyword, domainId, recursive, searchFilter); + return getUserListInternal(caller, permittedAccounts, listAll, id, username, type, accountName, state, keyword, apiKeyAccess, domainId, recursive, searchFilter); } private Pair, Integer> getUserListInternal(Account caller, List permittedAccounts, boolean listAll, Long id, Object username, Object type, - String accountName, Object state, String keyword, Long domainId, boolean recursive, Filter searchFilter) { + String accountName, Object state, String keyword, String apiKeyAccess, Long domainId, boolean recursive, Filter searchFilter) { Ternary domainIdRecursiveListProject = new Ternary(domainId, recursive, null); accountMgr.buildACLSearchParameters(caller, id, accountName, null, permittedAccounts, domainIdRecursiveListProject, listAll, false); domainId = domainIdRecursiveListProject.first(); @@ -757,6 +761,9 @@ private Pair, Integer> getUserListInternal(Account calle sb.and("domainId", sb.entity().getDomainId(), Op.EQ); sb.and("accountName", sb.entity().getAccountName(), Op.EQ); sb.and("state", sb.entity().getState(), Op.EQ); + if (apiKeyAccess != null) { + sb.and("apiKeyAccess", sb.entity().getApiKeyAccess(), Op.EQ); + } if ((accountName == null) && (domainId != null)) { sb.and("domainPath", sb.entity().getDomainPath(), Op.LIKE); @@ -811,6 +818,15 @@ private Pair, Integer> getUserListInternal(Account calle sc.setParameters("state", state); } + if (apiKeyAccess != null) { + try { + ApiConstants.ApiKeyAccess access = ApiConstants.ApiKeyAccess.valueOf(apiKeyAccess.toUpperCase()); + sc.setParameters("apiKeyAccess", access.toBoolean()); + } catch (IllegalArgumentException ex) { + throw new InvalidParameterValueException("ApiKeyAccess value can only be Enabled/Disabled/Inherit"); + } + } + return _userAccountJoinDao.searchAndCount(sc, searchFilter); } @@ -2897,6 +2913,7 @@ private Pair, Integer> searchForAccountIdsAndCount(ListAccountsCmd cm Object state = cmd.getState(); Object isCleanupRequired = cmd.isCleanupRequired(); Object keyword = cmd.getKeyword(); + String apiKeyAccess = cmd.getApiKeyAccess(); SearchBuilder accountSearchBuilder = _accountDao.createSearchBuilder(); accountSearchBuilder.select(null, Func.DISTINCT, accountSearchBuilder.entity().getId()); // select distinct @@ -2909,6 +2926,9 @@ private Pair, Integer> searchForAccountIdsAndCount(ListAccountsCmd cm accountSearchBuilder.and("typeNEQ", accountSearchBuilder.entity().getType(), SearchCriteria.Op.NEQ); accountSearchBuilder.and("idNEQ", accountSearchBuilder.entity().getId(), SearchCriteria.Op.NEQ); accountSearchBuilder.and("type2NEQ", accountSearchBuilder.entity().getType(), SearchCriteria.Op.NEQ); + if (apiKeyAccess != null) { + accountSearchBuilder.and("apiKeyAccess", accountSearchBuilder.entity().getApiKeyAccess(), Op.EQ); + } if (domainId != null && isRecursive) { SearchBuilder domainSearch = _domainDao.createSearchBuilder(); @@ -2972,6 +2992,15 @@ private Pair, Integer> searchForAccountIdsAndCount(ListAccountsCmd cm } } + if (apiKeyAccess != null) { + try { + ApiConstants.ApiKeyAccess access = ApiConstants.ApiKeyAccess.valueOf(apiKeyAccess.toUpperCase()); + sc.setParameters("apiKeyAccess", access.toBoolean()); + } catch (IllegalArgumentException ex) { + throw new InvalidParameterValueException("ApiKeyAccess value can only be Enabled/Disabled/Inherit"); + } + } + Pair, Integer> uniqueAccountPair = _accountDao.searchAndCount(sc, searchFilter); Integer count = uniqueAccountPair.second(); List accountIds = uniqueAccountPair.first().stream().map(AccountVO::getId).collect(Collectors.toList()); diff --git a/server/src/main/java/com/cloud/api/query/ViewResponseHelper.java b/server/src/main/java/com/cloud/api/query/ViewResponseHelper.java index db650bf7c3ef..7d5658f6782c 100644 --- a/server/src/main/java/com/cloud/api/query/ViewResponseHelper.java +++ b/server/src/main/java/com/cloud/api/query/ViewResponseHelper.java @@ -105,13 +105,13 @@ public class ViewResponseHelper { protected Logger logger = LogManager.getLogger(getClass()); public static List createUserResponse(UserAccountJoinVO... users) { - return createUserResponse(null, users); + return createUserResponse(ResponseView.Restricted, null, users); } - public static List createUserResponse(Long domainId, UserAccountJoinVO... users) { + public static List createUserResponse(ResponseView responseView, Long domainId, UserAccountJoinVO... users) { List respList = new ArrayList(); for (UserAccountJoinVO vt : users) { - respList.add(ApiDBUtils.newUserResponse(vt, domainId)); + respList.add(ApiDBUtils.newUserResponse(responseView, domainId, vt)); } return respList; } diff --git a/server/src/main/java/com/cloud/api/query/dao/AccountJoinDaoImpl.java b/server/src/main/java/com/cloud/api/query/dao/AccountJoinDaoImpl.java index 7ffd3ef319fe..07b5c27438b0 100644 --- a/server/src/main/java/com/cloud/api/query/dao/AccountJoinDaoImpl.java +++ b/server/src/main/java/com/cloud/api/query/dao/AccountJoinDaoImpl.java @@ -82,6 +82,9 @@ public AccountResponse newAccountResponse(ResponseView view, EnumSet { - UserResponse newUserResponse(UserAccountJoinVO usr); + UserResponse newUserResponse(ResponseObject.ResponseView responseView, UserAccountJoinVO usr); UserAccountJoinVO newUserView(User usr); diff --git a/server/src/main/java/com/cloud/api/query/dao/UserAccountJoinDaoImpl.java b/server/src/main/java/com/cloud/api/query/dao/UserAccountJoinDaoImpl.java index c5b21f50d2d9..f2c234b4c7cb 100644 --- a/server/src/main/java/com/cloud/api/query/dao/UserAccountJoinDaoImpl.java +++ b/server/src/main/java/com/cloud/api/query/dao/UserAccountJoinDaoImpl.java @@ -20,6 +20,7 @@ import com.cloud.user.AccountManagerImpl; +import org.apache.cloudstack.api.ResponseObject.ResponseView; import org.springframework.stereotype.Component; import org.apache.cloudstack.api.response.UserResponse; @@ -52,7 +53,7 @@ protected UserAccountJoinDaoImpl() { } @Override - public UserResponse newUserResponse(UserAccountJoinVO usr) { + public UserResponse newUserResponse(ResponseView view, UserAccountJoinVO usr) { UserResponse userResponse = new UserResponse(); userResponse.setAccountId(usr.getAccountUuid()); userResponse.setAccountName(usr.getAccountName()); @@ -75,6 +76,9 @@ public UserResponse newUserResponse(UserAccountJoinVO usr) { long domainId = usr.getDomainId(); boolean is2FAmandated = Boolean.TRUE.equals(AccountManagerImpl.enableUserTwoFactorAuthentication.valueIn(domainId)) && Boolean.TRUE.equals(AccountManagerImpl.mandateUserTwoFactorAuthentication.valueIn(domainId)); userResponse.set2FAmandated(is2FAmandated); + if (view == ResponseView.Full) { + userResponse.setApiKeyAccess(usr.getApiKeyAccess()); + } // set async job if (usr.getJobId() != null) { diff --git a/server/src/main/java/com/cloud/api/query/vo/AccountJoinVO.java b/server/src/main/java/com/cloud/api/query/vo/AccountJoinVO.java index 19c49294c844..0bd28d2af32d 100644 --- a/server/src/main/java/com/cloud/api/query/vo/AccountJoinVO.java +++ b/server/src/main/java/com/cloud/api/query/vo/AccountJoinVO.java @@ -189,6 +189,9 @@ public class AccountJoinVO extends BaseViewVO implements InternalIdentity, Ident @Column(name = "default") boolean isDefault; + @Column(name = "api_key_access") + Boolean apiKeyAccess; + public AccountJoinVO() { } @@ -393,4 +396,8 @@ public int getJobStatus() { public boolean isDefault() { return isDefault; } + + public Boolean getApiKeyAccess() { + return apiKeyAccess; + } } diff --git a/server/src/main/java/com/cloud/api/query/vo/UserAccountJoinVO.java b/server/src/main/java/com/cloud/api/query/vo/UserAccountJoinVO.java index 3a82980725b3..ad005eebb76e 100644 --- a/server/src/main/java/com/cloud/api/query/vo/UserAccountJoinVO.java +++ b/server/src/main/java/com/cloud/api/query/vo/UserAccountJoinVO.java @@ -133,6 +133,9 @@ public class UserAccountJoinVO extends BaseViewVO implements InternalIdentity, I @Column(name = "is_user_2fa_enabled") boolean user2faEnabled; + @Column(name = "api_key_access") + Boolean apiKeyAccess; + public UserAccountJoinVO() { } @@ -281,4 +284,8 @@ public User.Source getSource() { public boolean isUser2faEnabled() { return user2faEnabled; } + + public Boolean getApiKeyAccess() { + return apiKeyAccess; + } } diff --git a/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java b/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java index 31b9a7624add..02abc507fdbc 100644 --- a/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java +++ b/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java @@ -53,6 +53,7 @@ import org.apache.cloudstack.agent.lb.IndirectAgentLBServiceImpl; import org.apache.cloudstack.annotation.AnnotationService; import org.apache.cloudstack.annotation.dao.AnnotationDao; +import org.apache.cloudstack.api.ApiCommandResourceType; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.command.admin.config.ResetCfgCmd; import org.apache.cloudstack.api.command.admin.config.UpdateCfgCmd; @@ -310,6 +311,7 @@ import static com.cloud.configuration.Config.SecStorageAllowedInternalDownloadSites; import static com.cloud.offering.NetworkOffering.RoutingMode.Dynamic; import static com.cloud.offering.NetworkOffering.RoutingMode.Static; +import static org.apache.cloudstack.framework.config.ConfigKey.CATEGORY_SYSTEM; public class ConfigurationManagerImpl extends ManagerBase implements ConfigurationManager, ConfigurationService, Configurable { public static final String PERACCOUNT = "peraccount"; @@ -708,6 +710,7 @@ public String updateConfiguration(final long userId, final String name, final St value = DBEncryptionUtil.encrypt(value); } + ApiCommandResourceType resourceType; ConfigKey.Scope scopeVal = ConfigKey.Scope.valueOf(scope); switch (scopeVal) { case Zone: @@ -715,6 +718,7 @@ public String updateConfiguration(final long userId, final String name, final St if (zone == null) { throw new InvalidParameterValueException("unable to find zone by id " + resourceId); } + resourceType = ApiCommandResourceType.Zone; _dcDetailsDao.addDetail(resourceId, name, value, true); break; case Cluster: @@ -722,6 +726,7 @@ public String updateConfiguration(final long userId, final String name, final St if (cluster == null) { throw new InvalidParameterValueException("unable to find cluster by id " + resourceId); } + resourceType = ApiCommandResourceType.Cluster; String newName = name; if (name.equalsIgnoreCase("cpu.overprovisioning.factor")) { newName = "cpuOvercommitRatio"; @@ -744,6 +749,7 @@ public String updateConfiguration(final long userId, final String name, final St if (pool == null) { throw new InvalidParameterValueException("unable to find storage pool by id " + resourceId); } + resourceType = ApiCommandResourceType.StoragePool; if(name.equals(CapacityManager.StorageOverprovisioningFactor.key())) { if(!pool.getPoolType().supportsOverProvisioning() ) { throw new InvalidParameterValueException("Unable to update storage pool with id " + resourceId + ". Overprovision not supported for " + pool.getPoolType()); @@ -765,6 +771,7 @@ public String updateConfiguration(final long userId, final String name, final St if (account == null) { throw new InvalidParameterValueException("unable to find account by id " + resourceId); } + resourceType = ApiCommandResourceType.Account; AccountDetailVO accountDetailVO = _accountDetailsDao.findDetail(resourceId, name); if (accountDetailVO == null) { accountDetailVO = new AccountDetailVO(resourceId, name, value); @@ -778,6 +785,7 @@ public String updateConfiguration(final long userId, final String name, final St case ImageStore: final ImageStoreVO imgStore = _imageStoreDao.findById(resourceId); Preconditions.checkState(imgStore != null); + resourceType = ApiCommandResourceType.ImageStore; _imageStoreDetailsDao.addDetail(resourceId, name, value, true); break; @@ -786,6 +794,7 @@ public String updateConfiguration(final long userId, final String name, final St if (domain == null) { throw new InvalidParameterValueException("unable to find domain by id " + resourceId); } + resourceType = ApiCommandResourceType.Domain; DomainDetailVO domainDetailVO = _domainDetailsDao.findDetail(resourceId, name); if (domainDetailVO == null) { domainDetailVO = new DomainDetailVO(resourceId, name, value); @@ -800,6 +809,10 @@ public String updateConfiguration(final long userId, final String name, final St throw new InvalidParameterValueException("Scope provided is invalid"); } + CallContext.current().setEventResourceType(resourceType); + CallContext.current().setEventResourceId(resourceId); + CallContext.current().setEventDetails(String.format(" Name: %s, New Value: %s, Scope: %s", name, value, scope)); + _configDepot.invalidateConfigCache(name, scopeVal, resourceId); return valueEncrypted ? DBEncryptionUtil.decrypt(value) : value; } @@ -957,6 +970,11 @@ public Configuration updateConfiguration(final UpdateCfgCmd cmd) throws InvalidP category = config.getCategory(); } + if (CATEGORY_SYSTEM.equals(category) && !_accountMgr.isRootAdmin(caller.getId())) { + logger.warn("Only Root Admin is allowed to edit the configuration " + name); + throw new CloudRuntimeException("Only Root Admin is allowed to edit this configuration."); + } + if (value == null) { return _configDao.findByName(name); } @@ -1008,7 +1026,6 @@ public Configuration updateConfiguration(final UpdateCfgCmd cmd) throws InvalidP if (value.isEmpty() || value.equals("null")) { value = (id == null) ? null : ""; } - final String updatedValue = updateConfiguration(userId, name, category, value, scope, id); if (value == null && updatedValue == null || updatedValue.equalsIgnoreCase(value)) { return _configDao.findByName(name); diff --git a/server/src/main/java/com/cloud/user/AccountManagerImpl.java b/server/src/main/java/com/cloud/user/AccountManagerImpl.java index 39e8518f7607..fa177428e513 100644 --- a/server/src/main/java/com/cloud/user/AccountManagerImpl.java +++ b/server/src/main/java/com/cloud/user/AccountManagerImpl.java @@ -373,6 +373,13 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M "totp", "The default user two factor authentication provider. Eg. totp, staticpin", true, ConfigKey.Scope.Domain); + public static final ConfigKey apiKeyAccess = new ConfigKey<>(ConfigKey.CATEGORY_SYSTEM, Boolean.class, + "api.key.access", + "true", + "Determines whether API (api-key/secret-key) access is allowed or not. Editable only by Root Admin.", + true, + ConfigKey.Scope.Domain); + protected AccountManagerImpl() { super(); } @@ -1463,6 +1470,7 @@ public UserAccount updateUser(UpdateUserCmd updateUserCmd) { logger.debug("Updating user with Id: " + user.getUuid()); validateAndUpdateApiAndSecretKeyIfNeeded(updateUserCmd, user); + validateAndUpdateUserApiKeyAccess(updateUserCmd, user); Account account = retrieveAndValidateAccount(user); validateAndUpdateFirstNameIfNeeded(updateUserCmd, user); @@ -1682,6 +1690,38 @@ protected void validateAndUpdateApiAndSecretKeyIfNeeded(UpdateUserCmd updateUser user.setSecretKey(secretKey); } + protected void validateAndUpdateUserApiKeyAccess(UpdateUserCmd updateUserCmd, UserVO user) { + if (updateUserCmd.getApiKeyAccess() != null) { + try { + ApiConstants.ApiKeyAccess access = ApiConstants.ApiKeyAccess.valueOf(updateUserCmd.getApiKeyAccess().toUpperCase()); + user.setApiKeyAccess(access.toBoolean()); + Long callingUserId = CallContext.current().getCallingUserId(); + Account callingAccount = CallContext.current().getCallingAccount(); + ActionEventUtils.onActionEvent(callingUserId, callingAccount.getAccountId(), callingAccount.getDomainId(), + EventTypes.API_KEY_ACCESS_UPDATE, "Api key access was changed for the User to " + access.toString(), + user.getId(), ApiCommandResourceType.User.toString()); + } catch (IllegalArgumentException ex) { + throw new InvalidParameterValueException("ApiKeyAccess value can only be Enabled/Disabled/Inherit"); + } + } + } + + protected void validateAndUpdateAccountApiKeyAccess(UpdateAccountCmd updateAccountCmd, AccountVO account) { + if (updateAccountCmd.getApiKeyAccess() != null) { + try { + ApiConstants.ApiKeyAccess access = ApiConstants.ApiKeyAccess.valueOf(updateAccountCmd.getApiKeyAccess().toUpperCase()); + account.setApiKeyAccess(access.toBoolean()); + Long callingUserId = CallContext.current().getCallingUserId(); + Account callingAccount = CallContext.current().getCallingAccount(); + ActionEventUtils.onActionEvent(callingUserId, callingAccount.getAccountId(), callingAccount.getDomainId(), + EventTypes.API_KEY_ACCESS_UPDATE, "Api key access was changed for the Account to " + access.toString(), + account.getId(), ApiCommandResourceType.Account.toString()); + } catch (IllegalArgumentException ex) { + throw new InvalidParameterValueException("ApiKeyAccess value can only be Enabled/Disabled/Inherit"); + } + } + } + /** * Searches for a user with the given userId. If no user is found we throw an {@link InvalidParameterValueException}. */ @@ -2048,6 +2088,8 @@ public AccountVO updateAccount(UpdateAccountCmd cmd) { Account caller = getCurrentCallingAccount(); checkAccess(caller, _domainMgr.getDomain(account.getDomainId())); + validateAndUpdateAccountApiKeyAccess(cmd, acctForUpdate); + if(newAccountName != null) { if (newAccountName.isEmpty()) { @@ -2794,18 +2836,18 @@ public Pair findUserByApiKey(String apiKey) { } @Override - public Map getKeys(GetUserKeysCmd cmd) { + public Pair> getKeys(GetUserKeysCmd cmd) { final long userId = cmd.getID(); return getKeys(userId); } @Override - public Map getKeys(Long userId) { + public Pair> getKeys(Long userId) { User user = getActiveUser(userId); if (user == null) { throw new InvalidParameterValueException("Unable to find user by id"); } - final ControlledEntity account = getAccount(getUserAccountById(userId).getAccountId()); //Extracting the Account from the userID of the requested user. + final Account account = getAccount(getUserAccountById(userId).getAccountId()); //Extracting the Account from the userID of the requested user. User caller = CallContext.current().getCallingUser(); preventRootDomainAdminAccessToRootAdminKeys(caller, account); checkAccess(caller, account); @@ -2814,7 +2856,15 @@ public Map getKeys(Long userId) { keys.put("apikey", user.getApiKey()); keys.put("secretkey", user.getSecretKey()); - return keys; + Boolean apiKeyAccess = user.getApiKeyAccess(); + if (apiKeyAccess == null) { + apiKeyAccess = account.getApiKeyAccess(); + if (apiKeyAccess == null) { + apiKeyAccess = AccountManagerImpl.apiKeyAccess.valueIn(account.getDomainId()); + } + } + + return new Pair>(apiKeyAccess, keys); } protected void preventRootDomainAdminAccessToRootAdminKeys(User caller, ControlledEntity account) { @@ -3320,7 +3370,7 @@ public String getConfigComponentName() { @Override public ConfigKey[] getConfigKeys() { return new ConfigKey[] {UseSecretKeyInResponse, enableUserTwoFactorAuthentication, - userTwoFactorAuthenticationDefaultProvider, mandateUserTwoFactorAuthentication, userTwoFactorAuthenticationIssuer}; + userTwoFactorAuthenticationDefaultProvider, mandateUserTwoFactorAuthentication, userTwoFactorAuthenticationIssuer, apiKeyAccess}; } public List getUserTwoFactorAuthenticationProviders() { diff --git a/server/src/test/java/com/cloud/api/ApiServerTest.java b/server/src/test/java/com/cloud/api/ApiServerTest.java index fed1d95a625e..dedd6e02ec5c 100644 --- a/server/src/test/java/com/cloud/api/ApiServerTest.java +++ b/server/src/test/java/com/cloud/api/ApiServerTest.java @@ -17,6 +17,8 @@ package com.cloud.api; import com.cloud.domain.Domain; +import com.cloud.user.Account; +import com.cloud.user.User; import com.cloud.user.UserAccount; import com.cloud.utils.exception.CloudRuntimeException; import org.apache.cloudstack.framework.config.ConfigKey; @@ -147,4 +149,31 @@ public void testForgotPasswordFailureInactiveDomain() { Mockito.when(domain.getState()).thenReturn(Domain.State.Inactive); apiServer.forgotPassword(userAccount, domain); } + + @Test + public void testVerifyApiKeyAccessAllowed() { + Long domainId = 1L; + User user = Mockito.mock(User.class); + Account account = Mockito.mock(Account.class); + + Mockito.when(user.getApiKeyAccess()).thenReturn(true); + Assert.assertEquals(true, apiServer.verifyApiKeyAccessAllowed(user, account)); + Mockito.verify(account, Mockito.never()).getApiKeyAccess(); + + Mockito.when(user.getApiKeyAccess()).thenReturn(false); + Assert.assertEquals(false, apiServer.verifyApiKeyAccessAllowed(user, account)); + Mockito.verify(account, Mockito.never()).getApiKeyAccess(); + + Mockito.when(user.getApiKeyAccess()).thenReturn(null); + Mockito.when(account.getApiKeyAccess()).thenReturn(true); + Assert.assertEquals(true, apiServer.verifyApiKeyAccessAllowed(user, account)); + + Mockito.when(user.getApiKeyAccess()).thenReturn(null); + Mockito.when(account.getApiKeyAccess()).thenReturn(false); + Assert.assertEquals(false, apiServer.verifyApiKeyAccessAllowed(user, account)); + + Mockito.when(user.getApiKeyAccess()).thenReturn(null); + Mockito.when(account.getApiKeyAccess()).thenReturn(null); + Assert.assertEquals(true, apiServer.verifyApiKeyAccessAllowed(user, account)); + } } diff --git a/server/src/test/java/com/cloud/api/query/QueryManagerImplTest.java b/server/src/test/java/com/cloud/api/query/QueryManagerImplTest.java index f5de105e22c4..42ea1ad45561 100644 --- a/server/src/test/java/com/cloud/api/query/QueryManagerImplTest.java +++ b/server/src/test/java/com/cloud/api/query/QueryManagerImplTest.java @@ -17,13 +17,18 @@ package com.cloud.api.query; +import com.cloud.api.ApiDBUtils; import com.cloud.api.query.dao.TemplateJoinDao; +import com.cloud.api.query.dao.UserAccountJoinDao; import com.cloud.api.query.dao.UserVmJoinDao; import com.cloud.api.query.vo.EventJoinVO; import com.cloud.api.query.vo.TemplateJoinVO; +import com.cloud.api.query.vo.UserAccountJoinVO; import com.cloud.api.query.vo.UserVmJoinVO; import com.cloud.dc.ClusterVO; import com.cloud.dc.dao.ClusterDao; +import com.cloud.domain.DomainVO; +import com.cloud.domain.dao.DomainDao; import com.cloud.event.EventVO; import com.cloud.event.dao.EventDao; import com.cloud.event.dao.EventJoinDao; @@ -45,6 +50,7 @@ import com.cloud.user.AccountVO; import com.cloud.user.User; import com.cloud.user.UserVO; +import com.cloud.user.dao.AccountDao; import com.cloud.utils.Pair; import com.cloud.utils.db.EntityManager; import com.cloud.utils.db.Filter; @@ -56,8 +62,11 @@ import org.apache.cloudstack.acl.SecurityChecker; import org.apache.cloudstack.api.ApiCommandResourceType; +import org.apache.cloudstack.api.ResponseObject; import org.apache.cloudstack.api.command.admin.storage.ListObjectStoragePoolsCmd; +import org.apache.cloudstack.api.command.admin.user.ListUsersCmd; import org.apache.cloudstack.api.command.admin.vm.ListAffectedVmsForStorageScopeChangeCmd; +import org.apache.cloudstack.api.command.user.account.ListAccountsCmd; import org.apache.cloudstack.api.command.user.bucket.ListBucketsCmd; import org.apache.cloudstack.api.command.user.event.ListEventsCmd; import org.apache.cloudstack.api.command.user.resource.ListDetailOptionsCmd; @@ -65,6 +74,7 @@ import org.apache.cloudstack.api.response.EventResponse; import org.apache.cloudstack.api.response.ListResponse; import org.apache.cloudstack.api.response.ObjectStoreResponse; +import org.apache.cloudstack.api.response.UserResponse; import org.apache.cloudstack.api.response.VirtualMachineResponse; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.storage.datastore.db.ObjectStoreDao; @@ -150,6 +160,15 @@ public class QueryManagerImplTest { @Mock UserVmJoinDao userVmJoinDao; + @Mock + UserAccountJoinDao userAccountJoinDao; + + @Mock + DomainDao domainDao; + + @Mock + AccountDao accountDao; + private AccountVO account; private UserVO user; @@ -477,4 +496,79 @@ public void testListAffectedVmsForScopeChange() { Assert.assertEquals(response.getResponses().get(0).getId(), instanceUuid); Assert.assertEquals(response.getResponses().get(0).getName(), vmName); } + + @Test + public void testSearchForUsers() { + ListUsersCmd cmd = Mockito.mock(ListUsersCmd.class); + String username = "Admin"; + String accountName = "Admin"; + Account.Type accountType = Account.Type.ADMIN; + Long domainId = 1L; + String apiKeyAccess = "Disabled"; + Mockito.when(cmd.getUsername()).thenReturn(username); + Mockito.when(cmd.getAccountName()).thenReturn(accountName); + Mockito.when(cmd.getAccountType()).thenReturn(accountType); + Mockito.when(cmd.getDomainId()).thenReturn(domainId); + Mockito.when(cmd.getApiKeyAccess()).thenReturn(apiKeyAccess); + + UserAccountJoinVO user = new UserAccountJoinVO(); + DomainVO domain = Mockito.mock(DomainVO.class); + SearchBuilder sb = Mockito.mock(SearchBuilder.class); + SearchCriteria sc = Mockito.mock(SearchCriteria.class); + List users = new ArrayList<>(); + Pair, Integer> result = new Pair<>(users, 0); + UserResponse response = Mockito.mock(UserResponse.class); + + Mockito.when(userAccountJoinDao.createSearchBuilder()).thenReturn(sb); + Mockito.when(sb.entity()).thenReturn(user); + Mockito.when(sb.create()).thenReturn(sc); + Mockito.when(userAccountJoinDao.searchAndCount(any(SearchCriteria.class), any(Filter.class))).thenReturn(result); + + queryManager.searchForUsers(ResponseObject.ResponseView.Restricted, cmd); + + Mockito.verify(sc).setParameters("username", username); + Mockito.verify(sc).setParameters("accountName", accountName); + Mockito.verify(sc).setParameters("type", accountType); + Mockito.verify(sc).setParameters("domainId", domainId); + Mockito.verify(sc).setParameters("apiKeyAccess", false); + Mockito.verify(userAccountJoinDao, Mockito.times(1)).searchAndCount( + any(SearchCriteria.class), any(Filter.class)); + } + + @Test + public void testSearchForAccounts() { + ListAccountsCmd cmd = Mockito.mock(ListAccountsCmd.class); + Long domainId = 1L; + String accountName = "Admin"; + Account.Type accountType = Account.Type.ADMIN; + String apiKeyAccess = "Enabled"; + Mockito.when(cmd.getId()).thenReturn(null); + Mockito.when(cmd.getDomainId()).thenReturn(domainId); + Mockito.when(cmd.getSearchName()).thenReturn(accountName); + Mockito.when(cmd.getAccountType()).thenReturn(accountType); + Mockito.when(cmd.getApiKeyAccess()).thenReturn(apiKeyAccess); + + DomainVO domain = Mockito.mock(DomainVO.class); + SearchBuilder sb = Mockito.mock(SearchBuilder.class); + SearchCriteria sc = Mockito.mock(SearchCriteria.class); + Pair, Integer> uniqueAccountPair = new Pair<>(new ArrayList<>(), 0); + Mockito.when(domainDao.findById(domainId)).thenReturn(domain); + Mockito.doNothing().when(accountManager).checkAccess(account, domain); + + Mockito.when(accountDao.createSearchBuilder()).thenReturn(sb); + Mockito.when(sb.entity()).thenReturn(account); + Mockito.when(sb.create()).thenReturn(sc); + Mockito.when(accountDao.searchAndCount(any(SearchCriteria.class), any(Filter.class))).thenReturn(uniqueAccountPair); + + try (MockedStatic apiDBUtilsMocked = Mockito.mockStatic(ApiDBUtils.class)) { + queryManager.searchForAccounts(cmd); + } + + Mockito.verify(sc).setParameters("domainId", domainId); + Mockito.verify(sc).setParameters("accountName", accountName); + Mockito.verify(sc).setParameters("type", accountType); + Mockito.verify(sc).setParameters("apiKeyAccess", true); + Mockito.verify(accountDao, Mockito.times(1)).searchAndCount( + any(SearchCriteria.class), any(Filter.class)); + } } diff --git a/server/src/test/java/com/cloud/user/AccountManagerImplTest.java b/server/src/test/java/com/cloud/user/AccountManagerImplTest.java index 9daa19206faa..11fc69c538ce 100644 --- a/server/src/test/java/com/cloud/user/AccountManagerImplTest.java +++ b/server/src/test/java/com/cloud/user/AccountManagerImplTest.java @@ -26,7 +26,9 @@ import java.util.List; import java.util.Map; +import com.cloud.event.ActionEventUtils; import org.apache.cloudstack.acl.SecurityChecker.AccessType; +import org.apache.cloudstack.api.command.admin.account.UpdateAccountCmd; import org.apache.cloudstack.api.command.admin.user.DeleteUserCmd; import org.apache.cloudstack.acl.ControlledEntity; @@ -90,6 +92,9 @@ public class AccountManagerImplTest extends AccountManagetImplTestBase { @Mock private UpdateUserCmd UpdateUserCmdMock; + @Mock + private UpdateAccountCmd UpdateAccountCmdMock; + private long userVoIdMock = 111l; @Mock private UserVO userVoMock; @@ -507,6 +512,46 @@ public void validateAndUpdatApiAndSecretKeyIfNeededTest() { Mockito.verify(userVoMock).setSecretKey(secretKey); } + @Test + public void validateAndUpdatUserApiKeyAccess() { + Mockito.doReturn("Enabled").when(UpdateUserCmdMock).getApiKeyAccess(); + try (MockedStatic eventUtils = Mockito.mockStatic(ActionEventUtils.class)) { + Mockito.when(ActionEventUtils.onActionEvent(Mockito.anyLong(), Mockito.anyLong(), + Mockito.anyLong(), + Mockito.anyString(), Mockito.anyString(), + Mockito.anyLong(), Mockito.anyString())).thenReturn(1L); + accountManagerImpl.validateAndUpdateUserApiKeyAccess(UpdateUserCmdMock, userVoMock); + } + + Mockito.verify(userVoMock).setApiKeyAccess(true); + } + + @Test(expected = InvalidParameterValueException.class) + public void validateAndUpdatUserApiKeyAccessInvalidParameter() { + Mockito.doReturn("False").when(UpdateUserCmdMock).getApiKeyAccess(); + accountManagerImpl.validateAndUpdateUserApiKeyAccess(UpdateUserCmdMock, userVoMock); + } + + @Test + public void validateAndUpdatAccountApiKeyAccess() { + Mockito.doReturn("Inherit").when(UpdateAccountCmdMock).getApiKeyAccess(); + try (MockedStatic eventUtils = Mockito.mockStatic(ActionEventUtils.class)) { + Mockito.when(ActionEventUtils.onActionEvent(Mockito.anyLong(), Mockito.anyLong(), + Mockito.anyLong(), + Mockito.anyString(), Mockito.anyString(), + Mockito.anyLong(), Mockito.anyString())).thenReturn(1L); + accountManagerImpl.validateAndUpdateAccountApiKeyAccess(UpdateAccountCmdMock, accountVoMock); + } + + Mockito.verify(accountVoMock).setApiKeyAccess(null); + } + + @Test(expected = InvalidParameterValueException.class) + public void validateAndUpdatAccountApiKeyAccessInvalidParameter() { + Mockito.doReturn("False").when(UpdateAccountCmdMock).getApiKeyAccess(); + accountManagerImpl.validateAndUpdateAccountApiKeyAccess(UpdateAccountCmdMock, accountVoMock); + } + @Test(expected = CloudRuntimeException.class) public void retrieveAndValidateAccountTestAccountNotFound() { Mockito.doReturn(accountMockId).when(userVoMock).getAccountId(); diff --git a/server/src/test/java/com/cloud/user/MockAccountManagerImpl.java b/server/src/test/java/com/cloud/user/MockAccountManagerImpl.java index bd6632af1ca5..30324b419864 100644 --- a/server/src/test/java/com/cloud/user/MockAccountManagerImpl.java +++ b/server/src/test/java/com/cloud/user/MockAccountManagerImpl.java @@ -450,12 +450,12 @@ public Long finalyzeAccountId(String accountName, Long domainId, Long projectId, } @Override - public Map getKeys(GetUserKeysCmd cmd) { + public Pair> getKeys(GetUserKeysCmd cmd) { return null; } @Override - public Map getKeys(Long userId) { + public Pair> getKeys(Long userId) { return null; } diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json index a4b5a860c084..e2f637bd410a 100644 --- a/ui/public/locales/en.json +++ b/ui/public/locales/en.json @@ -32,6 +32,7 @@ "label.accesskey": "Access key", "label.access.key": "Access key", "label.secret.key": "Secret key", +"label.apikeyaccess": "Api Key Access", "label.account": "Account", "label.account.and.security.group": "Account - security group", "label.account.id": "Account ID", @@ -882,6 +883,7 @@ "label.edge": "Edge", "label.edge.zone": "Edge Zone", "label.edit": "Edit", +"label.edit.account": "Edit Account", "label.edit.acl.list": "Edit ACL list", "label.edit.acl.rule": "Edit ACL rule", "label.edit.autoscale.vmprofile": "Edit AutoScale Instance Profile", @@ -3549,6 +3551,7 @@ "message.success.scale.kubernetes": "Successfully scaled Kubernetes cluster", "message.success.unmanage.instance": "Successfully unmanaged Instance", "message.success.unmanage.volume": "Successfully unmanaged Volume", +"message.success.update.account": "Successfully updated Account", "message.success.update.bgp.peer": "Successfully updated BGP peer", "message.success.update.bucket": "Successfully updated bucket", "message.success.update.condition": "Successfully updated condition", diff --git a/ui/src/components/view/InfoCard.vue b/ui/src/components/view/InfoCard.vue index 06775d8efaf7..974a278b4567 100644 --- a/ui/src/components/view/InfoCard.vue +++ b/ui/src/components/view/InfoCard.vue @@ -733,8 +733,18 @@ -