diff --git a/api/src/main/java/com/cloud/event/EventTypes.java b/api/src/main/java/com/cloud/event/EventTypes.java index 5e5309965c1..81ed185dae5 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 bb9838f137a..6be4d0a48f6 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 interface Account extends ControlledEntity, InternalIdentity, Identity { 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 60db7abb734..e2c3bed0c29 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 @@ package com.cloud.user; 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 @@ public interface AccountService { */ 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 422e264f10b..041b39ad272 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 interface User extends OwnedBy, InternalIdentity { 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 a6c6991be24..8f78fe5c4b4 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 class ApiConstants { 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 91cbb90e4da..3347a0d09f3 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 java.util.Map; 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 org.apache.cloudstack.region.RegionService; 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 class UpdateAccountCmd extends BaseCmd { return params; } + public String getApiKeyAccess() { + return apiKeyAccess; + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// @@ -131,7 +140,7 @@ public class UpdateAccountCmd extends BaseCmd { 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 3a3414d95d8..cdd239f72b5 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 @@ package org.apache.cloudstack.api.command.admin.user; 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 class GetUserKeysCmd extends BaseCmd{ 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 ef9e3fa2240..27a78c738c9 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 @@ package org.apache.cloudstack.api.command.admin.user; 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 class ListUsersCmd extends BaseListAccountResourcesCmd { return username; } + public String getApiKeyAccess() { + return apiKeyAccess; + } + public Boolean getShowIcon() { return showIcon != null ? showIcon : false; } @@ -87,7 +97,7 @@ public class ListUsersCmd extends BaseListAccountResourcesCmd { @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 c9e1e934152..3d7f51ae220 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 @@ package org.apache.cloudstack.api.command.admin.user; 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 class UpdateUserCmd extends BaseCmd { 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 0a962b19e4f..9157188fdee 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.ArrayList; 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 class ListAccountsCmd extends BaseListDomainResourcesCmd implements UserC 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 7a84e85a4a6..6fc098295f6 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 class AccountResponse extends BaseResponse implements ResourceLimitAndCou 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 5faedabfc16..dd17cc5cc8a 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 @@ package org.apache.cloudstack.api.response; 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 class RegisterResponse extends BaseResponse { 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 1a17f3b8698..df97a915700 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 class UserResponse extends BaseResponse implements SetResourceIconRespons 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 c93e43d9f37..88081494320 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 @@ package org.apache.cloudstack.query; 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 cb219007325..abf86043937 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.Upgrade41800to41810; 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 class DatabaseUpgradeChecker implements SystemIntegrityChecker { .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 00000000000..197ca1cb34c --- /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 f04b2bafbde..74a538565d7 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 class AccountVO implements Account { 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 69970bf2d2c..7dac26429ac 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 class UserVO implements User, Identity, InternalIdentity { 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 eed5572a0b2..f9ef5c40eba 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 @@ import java.util.List; @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 class AccountDaoImpl extends GenericDaoBase implements A u.setAccountId(rs.getLong(3)); u.setSecretKey(DBEncryptionUtil.decrypt(rs.getString(4))); u.setState(State.getValueOf(rs.getString(5))); + boolean apiKeyAccess = rs.getBoolean(6); + if (rs.wasNull()) { + u.setApiKeyAccess(null); + } else { + u.setApiKeyAccess(apiKeyAccess); + } - 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))); + 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 00000000000..d187b6fa043 --- /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 00000000000..31c4928d81b --- /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 87546a9d118..dc64380fb57 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 7eedc03712b..340cfa9055f 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 36a8050754c..00cf56345c8 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 3a5541654bb..7d27e6b77ce 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 class MockAccountManager extends ManagerBase implements AccountManager { } @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 a169ebc0f19..944f60d292c 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 class ApiDBUtils { } 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 72e97c3a6ee..98f87dfc3f0 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.exception.ExceptionProxyObject; 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 @@ public class ApiServer extends ManagerBase implements HttpRequestHandler, ApiSer } } + 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 class ApiServer extends ManagerBase implements HttpRequestHandler, ApiSer 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 25018bc2c36..976d3817a0a 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 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q * .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 class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q 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 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q 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 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q 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 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q 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 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q 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 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q 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 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q 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 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q } } + 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 db650bf7c3e..7d5658f6782 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 7ffd3ef319f..07b5c27438b 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 class AccountJoinDaoImpl extends GenericDaoBase impl accountResponse.setNetworkDomain(account.getNetworkDomain()); accountResponse.setDefaultZone(account.getDataCenterUuid()); accountResponse.setIsDefault(account.isDefault()); + if (view == ResponseView.Full) { + accountResponse.setApiKeyAccess(account.getApiKeyAccess()); + } // get network stat accountResponse.setBytesReceived(account.getBytesReceived()); diff --git a/server/src/main/java/com/cloud/api/query/dao/UserAccountJoinDao.java b/server/src/main/java/com/cloud/api/query/dao/UserAccountJoinDao.java index b48f19272bc..cff758d0c17 100644 --- a/server/src/main/java/com/cloud/api/query/dao/UserAccountJoinDao.java +++ b/server/src/main/java/com/cloud/api/query/dao/UserAccountJoinDao.java @@ -18,6 +18,7 @@ package com.cloud.api.query.dao; import java.util.List; +import org.apache.cloudstack.api.ResponseObject; import org.apache.cloudstack.api.response.UserResponse; import com.cloud.api.query.vo.UserAccountJoinVO; @@ -27,7 +28,7 @@ import com.cloud.utils.db.GenericDao; public interface UserAccountJoinDao extends GenericDao { - 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 c5b21f50d2d..f2c234b4c7c 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 java.util.List; 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 @@ public class UserAccountJoinDaoImpl extends GenericDaoBase 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 class AccountManagerImpl extends ManagerBase implements AccountManager, M 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 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M 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 class AccountManagerImpl extends ManagerBase implements AccountManager, M Account caller = getCurrentCallingAccount(); checkAccess(caller, _domainMgr.getDomain(account.getDomainId())); + validateAndUpdateAccountApiKeyAccess(cmd, acctForUpdate); + if(newAccountName != null) { if (newAccountName.isEmpty()) { @@ -2794,18 +2836,18 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M } @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 class AccountManagerImpl extends ManagerBase implements AccountManager, M 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 class AccountManagerImpl extends ManagerBase implements AccountManager, M @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 fed1d95a625..dedd6e02ec5 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 class ApiServerTest { 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 f5de105e22c..42ea1ad4556 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.AccountManager; 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 com.cloud.vm.dao.VMInstanceDao; 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.DetailOptionsResponse; 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 class QueryManagerImplTest { 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 9daa19206fa..11fc69c538c 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.HashMap; 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 class AccountManagerImplTest extends AccountManagetImplTestBase { 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 bd6632af1ca..30324b41986 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 class MockAccountManagerImpl extends ManagerBase implements Manager, Acco } @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 a4b5a860c08..e2f637bd410 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 06775d8efaf..974a278b456 100644 --- a/ui/src/components/view/InfoCard.vue +++ b/ui/src/components/view/InfoCard.vue @@ -733,8 +733,18 @@ -