This is an automated email from the ASF dual-hosted git repository.
rohit pushed a commit to branch 4.20
in repository https://gitbox.apache.org/repos/asf/cloudstack.git
The following commit(s) were added to refs/heads/4.20 by this push:
new d17de834a57 Disable API Key Access for users, accounts and domains
(#9741)
d17de834a57 is described below
commit d17de834a57237b79f24850f97190bfc378f31a9
Author: Abhisar Sinha <[email protected]>
AuthorDate: Tue Dec 3 12:10:54 2024 +0530
Disable API Key Access for users, accounts and domains (#9741)
* cli changes to update user/account, list by apikeyaccess, domain level
setting
* UI changes for updating user/account and searchfilter in listview
* make the api parameters and setting accessible only to root admin
* revert changes to ui/package-lock.json
* minor changes to description strings
* UT for ApiServer and AccountManagerImpl classes
* fix pre-commit failure
* Added a constant for the string System
* UT for searchForUsers and searchForAccounts
* Fix marvin test error
* Update schema to use idempotent add column
* Fix `updateTemplatePermission` when the UI is set to a language other
than English (#9766)
* Fix updateTemplatePermission UI in non-english language
* Improve fix
---------
Co-authored-by: Lucas Martins <[email protected]>
* Added user name uuid to logging
* Add events when api key access is changed via api or config setting
* fix the userid for api key access update event
* Fix ut failure after event logging
* Convert drop down to radio-button in edit user and account
* Add ApiKeyAccess status in User InfoCard for Users if Api key is generated
* Return apiKeyAccess in user and account response only for Root Admin
* fixed noredist build failure
* Show apikeyaccess on the left panel in the user view for root admins as
well
* don't show divider if apiKeyAccess is not shown to user
* Fix events generated to set Username, Account and Domain of the caller
correctly
* cli changes to update user/account, list by apikeyaccess, domain level
setting
* UI changes for updating user/account and searchfilter in listview
* make the api parameters and setting accessible only to root admin
* revert changes to ui/package-lock.json
* minor changes to description strings
* UT for ApiServer and AccountManagerImpl classes
* fix pre-commit failure
* Added a constant for the string System
* UT for searchForUsers and searchForAccounts
* Fix marvin test error
* Update schema to use idempotent add column
* Added user name uuid to logging
* Add events when api key access is changed via api or config setting
* fix the userid for api key access update event
* Fix ut failure after event logging
* Convert drop down to radio-button in edit user and account
* Add ApiKeyAccess status in User InfoCard for Users if Api key is generated
* Return apiKeyAccess in user and account response only for Root Admin
* fixed noredist build failure
* Show apikeyaccess on the left panel in the user view for root admins as
well
* don't show divider if apiKeyAccess is not shown to user
* Fix events generated to set Username, Account and Domain of the caller
correctly
* Added DB upgrade path from 42000 to 42010
---------
Co-authored-by: Daan Hoogland <[email protected]>
Co-authored-by: Lucas Martins
<[email protected]>
Co-authored-by: Lucas Martins <[email protected]>
---
api/src/main/java/com/cloud/event/EventTypes.java | 1 +
api/src/main/java/com/cloud/user/Account.java | 4 +
.../main/java/com/cloud/user/AccountService.java | 5 +-
api/src/main/java/com/cloud/user/User.java | 5 +
.../org/apache/cloudstack/api/ApiConstants.java | 27 +++
.../command/admin/account/UpdateAccountCmd.java | 15 +-
.../api/command/admin/user/GetUserKeysCmd.java | 9 +-
.../api/command/admin/user/ListUsersCmd.java | 16 +-
.../api/command/admin/user/UpdateUserCmd.java | 8 +
.../api/command/user/account/ListAccountsCmd.java | 8 +
.../cloudstack/api/response/AccountResponse.java | 8 +
.../cloudstack/api/response/RegisterResponse.java | 13 +-
.../cloudstack/api/response/UserResponse.java | 8 +
.../org/apache/cloudstack/query/QueryService.java | 3 +-
.../com/cloud/upgrade/DatabaseUpgradeChecker.java | 2 +
.../com/cloud/upgrade/dao/Upgrade42000to42010.java | 83 +++++++++
.../src/main/java/com/cloud/user/AccountVO.java | 13 ++
.../src/main/java/com/cloud/user/UserVO.java | 12 ++
.../java/com/cloud/user/dao/AccountDaoImpl.java | 30 +++-
.../META-INF/db/schema-42000to42010-cleanup.sql | 20 +++
.../resources/META-INF/db/schema-42000to42010.sql | 24 +++
.../META-INF/db/views/cloud.account_view.sql | 1 +
.../META-INF/db/views/cloud.user_view.sql | 1 +
.../cloudstack/framework/config/ConfigKey.java | 1 +
.../contrail/management/MockAccountManager.java | 4 +-
server/src/main/java/com/cloud/api/ApiDBUtils.java | 6 +-
server/src/main/java/com/cloud/api/ApiServer.java | 33 ++++
.../java/com/cloud/api/query/QueryManagerImpl.java | 43 ++++-
.../com/cloud/api/query/ViewResponseHelper.java | 6 +-
.../cloud/api/query/dao/AccountJoinDaoImpl.java | 3 +
.../cloud/api/query/dao/UserAccountJoinDao.java | 3 +-
.../api/query/dao/UserAccountJoinDaoImpl.java | 6 +-
.../java/com/cloud/api/query/vo/AccountJoinVO.java | 7 +
.../com/cloud/api/query/vo/UserAccountJoinVO.java | 7 +
.../configuration/ConfigurationManagerImpl.java | 19 ++-
.../java/com/cloud/user/AccountManagerImpl.java | 60 ++++++-
.../src/test/java/com/cloud/api/ApiServerTest.java | 29 ++++
.../com/cloud/api/query/QueryManagerImplTest.java | 94 ++++++++++
.../com/cloud/user/AccountManagerImplTest.java | 45 +++++
.../com/cloud/user/MockAccountManagerImpl.java | 4 +-
ui/public/locales/en.json | 3 +
ui/src/components/view/InfoCard.vue | 18 +-
ui/src/components/view/SearchView.vue | 13 +-
ui/src/config/section/account.js | 21 ++-
ui/src/config/section/user.js | 7 +
ui/src/views/iam/EditAccount.vue | 190 +++++++++++++++++++++
ui/src/views/iam/EditUser.vue | 18 +-
47 files changed, 894 insertions(+), 62 deletions(-)
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<String, String> getKeys(GetUserKeysCmd cmd);
+ public Pair<Boolean, Map<String, String>> getKeys(GetUserKeysCmd cmd);
- public Map<String, String> getKeys(Long userId);
+ public Pair<Boolean, Map<String, String>> 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<String, String> keys = _accountService.getKeys(this);
+ Pair<Boolean, Map<String, String>> 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<UserResponse> response =
_queryService.searchForUsers(this);
+ ListResponse<UserResponse> 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<String> 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<TaggedResourceLimitAndCountResponse> 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<TaggedResourceLimitAndCountResponse>
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<Boolean> 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<UserResponse> searchForUsers(ListUsersCmd cmd) throws
PermissionDeniedException;
+ ListResponse<UserResponse> searchForUsers(ResponseObject.ResponseView
responseView, ListUsersCmd cmd) throws PermissionDeniedException;
ListResponse<UserResponse> 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<AccountVO, Long> 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<AccountVO> AllFieldsSearch;
@@ -148,13 +148,25 @@ public class AccountDaoImpl extends
GenericDaoBase<AccountVO, Long> implements A
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<User, Account>(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<T> {
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<String, String> getKeys(GetUserKeysCmd cmd){
+ public Pair<Boolean, Map<String, String>> getKeys(GetUserKeysCmd cmd){
return null;
}
@Override
- public Map<String, String> getKeys(Long userId) {
+ public Pair<Boolean, Map<String, String>> 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<String, Object[]>
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<UserResponse> searchForUsers(ListUsersCmd cmd) throws
PermissionDeniedException {
+ public ListResponse<UserResponse> searchForUsers(ResponseView
responseView, ListUsersCmd cmd) throws PermissionDeniedException {
Pair<List<UserAccountJoinVO>, Integer> result =
searchForUsersInternal(cmd);
ListResponse<UserResponse> response = new ListResponse<UserResponse>();
- List<UserResponse> userResponses =
ViewResponseHelper.createUserResponse(CallContext.current().getCallingAccount().getDomainId(),
+ if (CallContext.current().getCallingAccount().getType() ==
Account.Type.ADMIN) {
+ responseView = ResponseView.Full;
+ }
+ List<UserResponse> 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<List<UserAccountJoinVO>, Integer> result =
getUserListInternal(caller, permittedAccounts, listAll, id, username, type,
accountName, state, keyword, domainId, recursive,
- null);
+ Pair<List<UserAccountJoinVO>, Integer> result =
getUserListInternal(caller, permittedAccounts, listAll, id,
+ username, type, accountName, state, keyword, null, domainId,
recursive, null);
ListResponse<UserResponse> response = new ListResponse<UserResponse>();
- List<UserResponse> userResponses =
ViewResponseHelper.createUserResponse(CallContext.current().getCallingAccount().getDomainId(),
+ List<UserResponse> 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<List<UserAccountJoinVO>, Integer> getUserListInternal(Account
caller, List<Long> 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<Long, Boolean, ListProjectResourcesCriteria>
domainIdRecursiveListProject = new Ternary<Long, Boolean,
ListProjectResourcesCriteria>(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<AccountVO> 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<DomainVO> 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<List<AccountVO>, Integer> uniqueAccountPair =
_accountDao.searchAndCount(sc, searchFilter);
Integer count = uniqueAccountPair.second();
List<Long> 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<UserResponse> createUserResponse(UserAccountJoinVO...
users) {
- return createUserResponse(null, users);
+ return createUserResponse(ResponseView.Restricted, null, users);
}
- public static List<UserResponse> createUserResponse(Long domainId,
UserAccountJoinVO... users) {
+ public static List<UserResponse> createUserResponse(ResponseView
responseView, Long domainId, UserAccountJoinVO... users) {
List<UserResponse> respList = new ArrayList<UserResponse>();
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<AccountJoinVO, Long> 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<UserAccountJoinVO,
Long> {
- 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<UserAccountJoinVO, Lo
}
@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 class UserAccountJoinDaoImpl extends
GenericDaoBase<UserAccountJoinVO, Lo
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 19c49294c84..0bd28d2af32 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 class AccountJoinVO extends BaseViewVO implements
InternalIdentity, Ident
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 3a82980725b..ad005eebb76 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 class UserAccountJoinVO extends BaseViewVO
implements InternalIdentity, I
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 31b9a7624ad..02abc507fdb 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.IndirectAgentLB;
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 com.googlecode.ipv6.IPv6Network;
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 class ConfigurationManagerImpl extends ManagerBase
implements Configurati
value = DBEncryptionUtil.encrypt(value);
}
+ ApiCommandResourceType resourceType;
ConfigKey.Scope scopeVal = ConfigKey.Scope.valueOf(scope);
switch (scopeVal) {
case Zone:
@@ -715,6 +718,7 @@ public class ConfigurationManagerImpl extends ManagerBase
implements Configurati
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 class ConfigurationManagerImpl extends ManagerBase
implements Configurati
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 class ConfigurationManagerImpl extends ManagerBase
implements Configurati
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 class ConfigurationManagerImpl extends ManagerBase
implements Configurati
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 class ConfigurationManagerImpl extends ManagerBase
implements Configurati
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 class ConfigurationManagerImpl extends ManagerBase
implements Configurati
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 class ConfigurationManagerImpl extends ManagerBase
implements Configurati
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 class ConfigurationManagerImpl extends ManagerBase
implements Configurati
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 class ConfigurationManagerImpl extends ManagerBase
implements Configurati
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 39e8518f760..fa177428e51 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<Boolean> 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<String, String> getKeys(GetUserKeysCmd cmd) {
+ public Pair<Boolean, Map<String, String>> getKeys(GetUserKeysCmd cmd) {
final long userId = cmd.getID();
return getKeys(userId);
}
@Override
- public Map<String, String> getKeys(Long userId) {
+ public Pair<Boolean, Map<String, String>> 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<Boolean, Map<String, String>>(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<UserTwoFactorAuthenticator>
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<UserAccountJoinVO> sb =
Mockito.mock(SearchBuilder.class);
+ SearchCriteria<UserAccountJoinVO> sc =
Mockito.mock(SearchCriteria.class);
+ List<UserAccountJoinVO> users = new ArrayList<>();
+ Pair<List<UserAccountJoinVO>, 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<AccountVO> sb = Mockito.mock(SearchBuilder.class);
+ SearchCriteria<AccountVO> sc = Mockito.mock(SearchCriteria.class);
+ Pair<List<AccountVO>, 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<ApiDBUtils> 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<ActionEventUtils> 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<ActionEventUtils> 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<String, String> getKeys(GetUserKeysCmd cmd) {
+ public Pair<Boolean, Map<String, String>> getKeys(GetUserKeysCmd cmd) {
return null;
}
@Override
- public Map<String, String> getKeys(Long userId) {
+ public Pair<Boolean, Map<String, String>> 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 @@
</div>
</div>
- <div class="account-center-tags" v-if="showKeys">
+ <div class="account-center-tags" v-if="showKeys ||
resource.apikeyaccess">
<a-divider/>
+ </div>
+ <div class="account-center-tags" v-if="resource.apikeyaccess &&
resource.account">
+ <div class="resource-detail-item">
+ <div class="resource-detail-item__label">{{ $t('label.apikeyaccess')
}}</div>
+ <div class="resource-detail-item__details">
+ <status class="status" :text="resource.apikeyaccess" displayText/>
+ </div>
+ </div>
+ </div>
+ <div class="account-center-tags" v-if="showKeys">
<div class="user-keys">
<key-outlined />
<strong>
@@ -1083,6 +1093,9 @@ export default {
api('getUserKeys', { id: this.resource.id }).then(json => {
this.showKeys = true
this.newResource.secretkey =
json.getuserkeysresponse.userkeys.secretkey
+ if (!this.isAdmin()) {
+ this.newResource.apikeyaccess =
json.getuserkeysresponse.userkeys.apikeyaccess ? 'Enabled' : 'Disabled'
+ }
this.$emit('change-resource', this.newResource)
})
},
@@ -1113,6 +1126,9 @@ export default {
(this.resource.domainid === this.$store.getters.userInfo.domainid &&
this.resource.account === this.$store.getters.userInfo.account) ||
(this.resource.project && this.resource.projectid ===
this.$store.getters.project.id)
},
+ isAdmin () {
+ return ['Admin'].includes(this.$store.getters.userInfo.roletype)
+ },
showInput () {
this.inputVisible = true
this.$nextTick(function () {
diff --git a/ui/src/components/view/SearchView.vue
b/ui/src/components/view/SearchView.vue
index c82b1172f2d..786a9f15be6 100644
--- a/ui/src/components/view/SearchView.vue
+++ b/ui/src/components/view/SearchView.vue
@@ -318,7 +318,7 @@ export default {
type = 'list'
} else if (item === 'tags') {
type = 'tag'
- } else if (item === 'resourcetype') {
+ } else if (['resourcetype', 'apikeyaccess'].includes(item)) {
type = 'autocomplete'
} else if (item === 'isencrypted') {
type = 'boolean'
@@ -431,6 +431,17 @@ export default {
]
this.fields[resourceTypeIndex].loading = false
}
+
+ if (arrayField.includes('apikeyaccess')) {
+ const apiKeyAccessIndex = this.fields.findIndex(item => item.name ===
'apikeyaccess')
+ this.fields[apiKeyAccessIndex].loading = true
+ this.fields[apiKeyAccessIndex].opts = [
+ { value: 'Disabled' },
+ { value: 'Enabled' },
+ { value: 'Inherit' }
+ ]
+ this.fields[apiKeyAccessIndex].loading = false
+ }
},
async fetchDynamicFieldData (arrayField, searchKeyword) {
const promises = []
diff --git a/ui/src/config/section/account.js b/ui/src/config/section/account.js
index 28c0e3f556d..21996167705 100644
--- a/ui/src/config/section/account.js
+++ b/ui/src/config/section/account.js
@@ -24,9 +24,15 @@ export default {
icon: 'team-outlined',
docHelp: 'adminguide/accounts.html',
permission: ['listAccounts'],
- searchFilters: ['name', 'accounttype', 'domainid'],
+ searchFilters: () => {
+ var filters = ['name', 'accounttype', 'domainid']
+ if (store.getters.userInfo.roletype === 'Admin') {
+ filters.push('apikeyaccess')
+ }
+ return filters
+ },
columns: ['name', 'state', 'rolename', 'roletype', 'domainpath'],
- details: ['name', 'id', 'rolename', 'roletype', 'domainpath',
'networkdomain', 'iptotal', 'vmtotal', 'volumetotal', 'receivedbytes',
'sentbytes', 'created'],
+ details: ['name', 'id', 'rolename', 'roletype', 'domainpath',
'networkdomain', 'apikeyaccess', 'iptotal', 'vmtotal', 'volumetotal',
'receivedbytes', 'sentbytes', 'created'],
related: [{
name: 'accountuser',
title: 'label.users',
@@ -116,15 +122,8 @@ export default {
icon: 'edit-outlined',
label: 'label.action.edit.account',
dataView: true,
- args: ['newname', 'account', 'domainid', 'networkdomain', 'roleid'],
- mapping: {
- account: {
- value: (record) => { return record.name }
- },
- domainid: {
- value: (record) => { return record.domainid }
- }
- }
+ popup: true,
+ component: shallowRef(defineAsyncComponent(() =>
import('@/views/iam/EditAccount.vue')))
},
{
api: 'updateResourceCount',
diff --git a/ui/src/config/section/user.js b/ui/src/config/section/user.js
index d8c4ac06d0c..60a55973f8c 100644
--- a/ui/src/config/section/user.js
+++ b/ui/src/config/section/user.js
@@ -25,6 +25,13 @@ export default {
docHelp: 'adminguide/accounts.html#users',
hidden: true,
permission: ['listUsers'],
+ searchFilters: () => {
+ var filters = []
+ if (store.getters.userInfo.roletype === 'Admin') {
+ filters.push('apikeyaccess')
+ }
+ return filters
+ },
columns: ['username', 'state', 'firstname', 'lastname', 'email', 'account',
'domain'],
details: ['username', 'id', 'firstname', 'lastname', 'email', 'usersource',
'timezone', 'rolename', 'roletype', 'is2faenabled', 'account', 'domain',
'created'],
tabs: [
diff --git a/ui/src/views/iam/EditAccount.vue b/ui/src/views/iam/EditAccount.vue
new file mode 100644
index 00000000000..7775bb2b825
--- /dev/null
+++ b/ui/src/views/iam/EditAccount.vue
@@ -0,0 +1,190 @@
+// 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.
+
+<template>
+ <div class="form-layout" v-ctrl-enter="handleSubmit">
+ <a-spin :spinning="loading">
+ <a-form
+ :ref="formRef"
+ :model="form"
+ :loading="loading"
+ layout="vertical"
+ @finish="handleSubmit">
+ <a-form-item ref="newname" name="newname">
+ <template #label>
+ <tooltip-label :title="$t('label.newname')"
:tooltip="apiParams.newname.description"/>
+ </template>
+ <a-input
+ v-model:value="form.newname"
+ :placeholder="apiParams.newname.description" />
+ </a-form-item>
+ <a-form-item ref="networkdomain" name="networkdomain">
+ <template #label>
+ <tooltip-label :title="$t('label.networkdomain')"
:tooltip="apiParams.networkdomain.description"/>
+ </template>
+ <a-input
+ v-model:value="form.networkdomain"
+ :placeholder="apiParams.networkdomain.description" />
+ </a-form-item>
+ <a-form-item ref="roleid" name="roleid">
+ <template #label>
+ <tooltip-label :title="$t('label.role')"
:tooltip="apiParams.roleid.description"/>
+ </template>
+ <a-select
+ v-model:value="form.roleid"
+ :loading="roleLoading"
+ :placeholder="apiParams.roleid.description"
+ v-focus="true"
+ showSearch
+ optionFilterProp="label"
+ :filterOption="(input, option) => {
+ return option.label.toLowerCase().indexOf(input.toLowerCase())
>= 0
+ }">
+ <a-select-option v-for="role in roles" :key="role.id"
:value="role.id">{{ role.name }}</a-select-option>
+ </a-select>
+ </a-form-item>
+ <a-form-item v-if="isRootAdmin" ref="apikeyaccess" name="apikeyaccess">
+ <template #label>
+ <tooltip-label :title="$t('label.apikeyaccess')"
:tooltip="apiParams.apikeyaccess.description"/>
+ </template>
+ <a-radio-group v-model:value="form.apikeyaccess" buttonStyle="solid">
+ <a-radio-button value="ENABLED">Enabled</a-radio-button>
+ <a-radio-button value="INHERIT">Inherit</a-radio-button>
+ <a-radio-button value="DISABLED">Disabled</a-radio-button>
+ </a-radio-group>
+ </a-form-item>
+ <div :span="24" class="action-button">
+ <a-button @click="closeAction">{{ $t('label.cancel') }}</a-button>
+ <a-button :loading="loading" ref="submit" type="primary"
@click="handleSubmit">{{ $t('label.ok') }}</a-button>
+ </div>
+ </a-form>
+ </a-spin>
+ </div>
+</template>
+<script>
+import { ref, reactive, toRaw } from 'vue'
+import { api } from '@/api'
+import TooltipLabel from '@/components/widgets/TooltipLabel'
+
+export default {
+ name: 'EditAccount',
+ components: {
+ TooltipLabel
+ },
+ props: {
+ resource: {
+ type: Object,
+ required: true
+ }
+ },
+ data () {
+ return {
+ loading: false,
+ roleLoading: false,
+ roles: []
+ }
+ },
+ beforeCreate () {
+ this.apiParams = this.$getApiParams('updateAccount')
+ },
+ created () {
+ this.initForm()
+ this.fetchData()
+ },
+ computed: {
+ isRootAdmin () {
+ return this.$store.getters.userInfo?.roletype === 'Admin'
+ }
+ },
+ methods: {
+ initForm () {
+ this.formRef = ref()
+ this.form = reactive({})
+ },
+ fetchData () {
+ this.account = this.resource.name
+ this.domainId = this.resource.domainid
+ this.form.apikeyaccess = this.resource.apikeyaccess
+ this.fetchRoles()
+ },
+ isValidValueForKey (obj, key) {
+ return key in obj && obj[key] != null
+ },
+ fetchRoles () {
+ this.roleLoading = true
+ const params = {}
+ params.state = 'enabled'
+ api('listRoles', params).then(response => {
+ this.roles = response.listrolesresponse.role || []
+ this.form.roleid = this.resource.roleid
+ }).finally(() => {
+ this.roleLoading = false
+ })
+ },
+ handleSubmit (e) {
+ e.preventDefault()
+ if (this.loading) return
+ this.formRef.value.validate().then(() => {
+ const values = toRaw(this.form)
+
+ this.loading = true
+ const params = {
+ newname: values.newname,
+ networkdomain: values.networkdomain,
+ roleid: values.roleid,
+ apikeyaccess: values.apikeyaccess,
+ account: this.account,
+ domainid: this.domainId
+ }
+ if (this.isValidValueForKey(values, 'networkdomain') &&
values.networkdomain.length > 0) {
+ params.networkdomain = values.networkdomain
+ }
+
+ api('updateAccount', params).then(response => {
+ this.$emit('refresh-data')
+ this.$notification.success({
+ message: this.$t('label.edit.account'),
+ description: `${this.$t('message.success.update.account')}
${params.account}`
+ })
+ this.closeAction()
+ }).catch(error => {
+ this.$notification.error({
+ message: this.$t('message.request.failed'),
+ description: (error.response && error.response.headers &&
error.response.headers['x-description']) || error.message,
+ duration: 0
+ })
+ }).finally(() => {
+ this.loading = false
+ })
+ }).catch(error => {
+ this.formRef.value.scrollToField(error.errorFields[0].name)
+ })
+ },
+ closeAction () {
+ this.$emit('close-action')
+ }
+ }
+}
+</script>
+<style scoped lang="less">
+ .form-layout {
+ width: 80vw;
+ @media (min-width: 600px) {
+ width: 450px;
+ }
+ }
+</style>
diff --git a/ui/src/views/iam/EditUser.vue b/ui/src/views/iam/EditUser.vue
index e082fd17a06..18f22b6f1e8 100644
--- a/ui/src/views/iam/EditUser.vue
+++ b/ui/src/views/iam/EditUser.vue
@@ -81,6 +81,16 @@
</a-select-option>
</a-select>
</a-form-item>
+ <a-form-item v-if="isRootAdmin" ref="apikeyaccess" name="apikeyaccess">
+ <template #label>
+ <tooltip-label :title="$t('label.apikeyaccess')"
:tooltip="apiParams.apikeyaccess.description"/>
+ </template>
+ <a-radio-group v-model:value="form.apikeyaccess" buttonStyle="solid">
+ <a-radio-button value="ENABLED">Enabled</a-radio-button>
+ <a-radio-button value="INHERIT">Inherit</a-radio-button>
+ <a-radio-button value="DISABLED">Disabled</a-radio-button>
+ </a-radio-group>
+ </a-form-item>
<div :span="24" class="action-button">
<a-button @click="closeAction">{{ $t('label.cancel') }}</a-button>
<a-button :loading="loading" ref="submit" type="primary"
@click="handleSubmit">{{ $t('label.ok') }}</a-button>
@@ -128,6 +138,11 @@ export default {
this.initForm()
this.fetchData()
},
+ computed: {
+ isRootAdmin () {
+ return this.$store.getters.userInfo?.roletype === 'Admin'
+ }
+ },
methods: {
initForm () {
this.formRef = ref()
@@ -187,7 +202,8 @@ export default {
username: values.username,
email: values.email,
firstname: values.firstname,
- lastname: values.lastname
+ lastname: values.lastname,
+ apikeyaccess: values.apikeyaccess
}
if (this.isValidValueForKey(values, 'timezone') &&
values.timezone.length > 0) {
params.timezone = values.timezone