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 <lucas.martins@scclouds.com.br>

* 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 <daan@onecht.net>
Co-authored-by: Lucas Martins <56271185+lucas-a-martins@users.noreply.github.com>
Co-authored-by: Lucas Martins <lucas.martins@scclouds.com.br>
This commit is contained in:
Abhisar Sinha 2024-12-03 12:10:54 +05:30 committed by GitHub
parent 58138f2da3
commit d17de834a5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
47 changed files with 893 additions and 61 deletions

View File

@ -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";

View File

@ -93,4 +93,8 @@ public interface Account extends ControlledEntity, InternalIdentity, Identity {
boolean isDefault();
public void setApiKeyAccess(Boolean apiKeyAccess);
public Boolean getApiKeyAccess();
}

View File

@ -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

View File

@ -94,4 +94,9 @@ public interface User extends OwnedBy, InternalIdentity {
public boolean isUser2faEnabled();
public String getKeyFor2fa();
public void setApiKeyAccess(Boolean apiKeyAccess);
public Boolean getApiKeyAccess();
}

View File

@ -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;
}
}
}
}

View File

@ -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 {

View File

@ -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");

View File

@ -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()) {

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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;

View File

@ -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();
}

View File

@ -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)");
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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)));
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<User, Account>(u, a);
}

View File

@ -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
--;

View File

@ -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" ');

View File

@ -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`,

View File

@ -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,

View File

@ -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

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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());

View File

@ -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;
}

View File

@ -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());

View File

@ -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);

View File

@ -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) {

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);

View File

@ -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() {

View File

@ -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));
}
}

View File

@ -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));
}
}

View File

@ -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();

View File

@ -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;
}

View File

@ -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",

View File

@ -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 () {

View File

@ -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 = []

View File

@ -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',

View File

@ -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: [

View File

@ -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>

View File

@ -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