diff --git a/api/src/main/java/com/cloud/user/Account.java b/api/src/main/java/com/cloud/user/Account.java index bb9838f137a9..6be4d0a48f6e 100644 --- a/api/src/main/java/com/cloud/user/Account.java +++ b/api/src/main/java/com/cloud/user/Account.java @@ -93,4 +93,8 @@ public static Type getFromValue(Integer type){ boolean isDefault(); + public void setApiKeyAccess(Boolean apiKeyAccess); + + public Boolean getApiKeyAccess(); + } diff --git a/api/src/main/java/com/cloud/user/User.java b/api/src/main/java/com/cloud/user/User.java index 422e264f10be..041b39ad2729 100644 --- a/api/src/main/java/com/cloud/user/User.java +++ b/api/src/main/java/com/cloud/user/User.java @@ -94,4 +94,9 @@ public enum Source { public boolean isUser2faEnabled(); public String getKeyFor2fa(); + + public void setApiKeyAccess(Boolean apiKeyAccess); + + public Boolean getApiKeyAccess(); + } diff --git a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java index bb16b0ff90de..e3324ea95d50 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"; @@ -1238,4 +1239,30 @@ public enum VMDetails { public enum DomainDetails { all, resource, min; } + + public enum ApiKeyAccess { + DISABLED(false), + ENABLED(true), + INHERIT(null); + + Boolean apiKeyAccess; + + ApiKeyAccess(Boolean keyAccess) { + apiKeyAccess = keyAccess; + } + + public Boolean toBoolean() { + return apiKeyAccess; + } + + public static ApiKeyAccess fromBoolean(Boolean value) { + if (value == null) { + return INHERIT; + } else if (value) { + return ENABLED; + } else { + return DISABLED; + } + } + } } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/account/UpdateAccountCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/account/UpdateAccountCmd.java index 91cbb90e4da4..324fff0e3824 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,6 +21,7 @@ import javax.inject.Inject; +import org.apache.cloudstack.acl.RoleType; import org.apache.cloudstack.api.ApiCommandResourceType; import org.apache.cloudstack.api.response.RoleResponse; @@ -70,6 +71,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 +113,10 @@ public Map getDetails() { return params; } + public String getApiKeyAccess() { + return apiKeyAccess; + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/user/ListUsersCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/user/ListUsersCmd.java index ef9e3fa22405..14dfc124762e 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,6 +19,7 @@ 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.response.ResourceIconResponse; import org.apache.cloudstack.api.APICommand; @@ -53,6 +54,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 +81,10 @@ public String getUsername() { return username; } + public String getApiKeyAccess() { + return apiKeyAccess; + } + public Boolean getShowIcon() { return showIcon != null ? showIcon : false; } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/user/UpdateUserCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/user/UpdateUserCmd.java index c9e1e934152d..3d7f51ae2204 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/user/UpdateUserCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/user/UpdateUserCmd.java @@ -18,6 +18,7 @@ import javax.inject.Inject; +import org.apache.cloudstack.acl.RoleType; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiCommandResourceType; import org.apache.cloudstack.api.ApiConstants; @@ -69,6 +70,9 @@ public class UpdateUserCmd extends BaseCmd { @Parameter(name = ApiConstants.USER_SECRET_KEY, type = CommandType.STRING, description = "The secret key for the user. Must be specified with userApiKey") private String secretKey; + @Parameter(name = ApiConstants.API_KEY_ACCESS, type = CommandType.STRING, description = "Determines if Api key access for this user is enabled, disabled or inherits the value from its parent, the owning account", since = "4.20.1.0", authorized = {RoleType.Admin}) + private String apiKeyAccess; + @Parameter(name = ApiConstants.TIMEZONE, type = CommandType.STRING, description = "Specifies a timezone for this command. For more information on the timezone parameter, see Time Zone Format.") @@ -120,6 +124,10 @@ public String getSecretKey() { return secretKey; } + public String getApiKeyAccess() { + return apiKeyAccess; + } + public String getTimezone() { return timezone; } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/account/ListAccountsCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/account/ListAccountsCmd.java index 0a962b19e4f3..9157188fdeee 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/account/ListAccountsCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/account/ListAccountsCmd.java @@ -20,6 +20,7 @@ import java.util.EnumSet; import java.util.List; +import org.apache.cloudstack.acl.RoleType; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiCommandResourceType; import org.apache.cloudstack.api.ApiConstants; @@ -70,6 +71,9 @@ public class ListAccountsCmd extends BaseListDomainResourcesCmd implements UserC description = "comma separated list of account details requested, value can be a list of [ all, resource, min]") private List viewDetails; + @Parameter(name = ApiConstants.API_KEY_ACCESS, type = CommandType.STRING, description = "List accounts by the Api key access value", since = "4.20.1.0", authorized = {RoleType.Admin}) + private String apiKeyAccess; + @Parameter(name = ApiConstants.SHOW_RESOURCE_ICON, type = CommandType.BOOLEAN, description = "flag to display the resource icon for accounts") private Boolean showIcon; @@ -120,6 +124,10 @@ public EnumSet getDetails() throws InvalidParameterValueException return dv; } + public String getApiKeyAccess() { + return apiKeyAccess; + } + public boolean getShowIcon() { return showIcon != null ? showIcon : false; } diff --git a/api/src/main/java/org/apache/cloudstack/api/response/AccountResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/AccountResponse.java index 7a84e85a4a6f..6fc098295f64 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/AccountResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/AccountResponse.java @@ -271,6 +271,10 @@ public class AccountResponse extends BaseResponse implements ResourceLimitAndCou @Param(description = "The tagged resource limit and count for the account", since = "4.20.0") List taggedResources; + @SerializedName(ApiConstants.API_KEY_ACCESS) + @Param(description = "whether api key access is Enabled, Disabled or set to Inherit (it inherits the value from the parent)", since = "4.20.1.0") + ApiConstants.ApiKeyAccess apiKeyAccess; + @Override public String getObjectId() { return id; @@ -554,4 +558,8 @@ public void setResourceIconResponse(ResourceIconResponse icon) { public void setTaggedResourceLimitsAndCounts(List taggedResourceLimitsAndCounts) { this.taggedResources = taggedResourceLimitsAndCounts; } + + public void setApiKeyAccess(Boolean apiKeyAccess) { + this.apiKeyAccess = ApiConstants.ApiKeyAccess.fromBoolean(apiKeyAccess); + } } diff --git a/api/src/main/java/org/apache/cloudstack/api/response/UserResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/UserResponse.java index 1a17f3b86988..df97a915700f 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/UserResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/UserResponse.java @@ -128,6 +128,10 @@ public class UserResponse extends BaseResponse implements SetResourceIconRespons @Param(description = "true if user has two factor authentication is mandated", since = "4.18.0.0") private Boolean is2FAmandated; + @SerializedName(ApiConstants.API_KEY_ACCESS) + @Param(description = "whether api key access is Enabled, Disabled or set to Inherit (it inherits the value from the parent)", since = "4.20.1.0") + ApiConstants.ApiKeyAccess apiKeyAccess; + @Override public String getObjectId() { return this.getId(); @@ -309,4 +313,8 @@ public Boolean getIs2FAmandated() { public void set2FAmandated(Boolean is2FAmandated) { this.is2FAmandated = is2FAmandated; } + + public void setApiKeyAccess(Boolean apiKeyAccess) { + this.apiKeyAccess = ApiConstants.ApiKeyAccess.fromBoolean(apiKeyAccess); + } } diff --git a/engine/schema/src/main/java/com/cloud/user/AccountVO.java b/engine/schema/src/main/java/com/cloud/user/AccountVO.java index f04b2bafbde6..74a538565d77 100644 --- a/engine/schema/src/main/java/com/cloud/user/AccountVO.java +++ b/engine/schema/src/main/java/com/cloud/user/AccountVO.java @@ -77,6 +77,9 @@ public class AccountVO implements Account { @Column(name = "default") boolean isDefault; + @Column(name = "api_key_access") + private Boolean apiKeyAccess; + public AccountVO() { uuid = UUID.randomUUID().toString(); } @@ -229,4 +232,14 @@ public String getName() { public String reflectionToString() { return ReflectionToStringBuilderUtils.reflectOnlySelectedFields(this, "id", "uuid", "accountName", "domainId"); } + + @Override + public void setApiKeyAccess(Boolean apiKeyAccess) { + this.apiKeyAccess = apiKeyAccess; + } + + @Override + public Boolean getApiKeyAccess() { + return apiKeyAccess; + } } diff --git a/engine/schema/src/main/java/com/cloud/user/UserVO.java b/engine/schema/src/main/java/com/cloud/user/UserVO.java index 69970bf2d2cd..7dac26429ace 100644 --- a/engine/schema/src/main/java/com/cloud/user/UserVO.java +++ b/engine/schema/src/main/java/com/cloud/user/UserVO.java @@ -115,6 +115,9 @@ public class UserVO implements User, Identity, InternalIdentity { @Column(name = "key_for_2fa") private String keyFor2fa; + @Column(name = "api_key_access") + private Boolean apiKeyAccess; + public UserVO() { this.uuid = UUID.randomUUID().toString(); } @@ -350,4 +353,13 @@ public void setUser2faProvider(String user2faProvider) { this.user2faProvider = user2faProvider; } + @Override + public void setApiKeyAccess(Boolean apiKeyAccess) { + this.apiKeyAccess = apiKeyAccess; + } + + @Override + public Boolean getApiKeyAccess() { + return apiKeyAccess; + } } diff --git a/engine/schema/src/main/java/com/cloud/user/dao/AccountDaoImpl.java b/engine/schema/src/main/java/com/cloud/user/dao/AccountDaoImpl.java index eed5572a0b24..f9ef5c40eba2 100644 --- a/engine/schema/src/main/java/com/cloud/user/dao/AccountDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/user/dao/AccountDaoImpl.java @@ -41,8 +41,8 @@ @Component public class AccountDaoImpl extends GenericDaoBase implements AccountDao { - private static final String FIND_USER_ACCOUNT_BY_API_KEY = "SELECT u.id, u.username, u.account_id, u.secret_key, u.state, " - + "a.id, a.account_name, a.type, a.role_id, a.domain_id, a.state " + "FROM `cloud`.`user` u, `cloud`.`account` a " + private static final String FIND_USER_ACCOUNT_BY_API_KEY = "SELECT u.id, u.username, u.account_id, u.secret_key, u.state, u.api_key_access, " + + "a.id, a.account_name, a.type, a.role_id, a.domain_id, a.state, a.api_key_access " + "FROM `cloud`.`user` u, `cloud`.`account` a " + "WHERE u.account_id = a.id AND u.api_key = ? and u.removed IS NULL"; protected final SearchBuilder AllFieldsSearch; @@ -148,13 +148,25 @@ public Pair findUserAccountByApiKey(String apiKey) { u.setAccountId(rs.getLong(3)); u.setSecretKey(DBEncryptionUtil.decrypt(rs.getString(4))); u.setState(State.getValueOf(rs.getString(5))); - - AccountVO a = new AccountVO(rs.getLong(6)); - a.setAccountName(rs.getString(7)); - a.setType(Account.Type.getFromValue(rs.getInt(8))); - a.setRoleId(rs.getLong(9)); - a.setDomainId(rs.getLong(10)); - a.setState(State.getValueOf(rs.getString(11))); + boolean apiKeyAccess = rs.getBoolean(6); + if (rs.wasNull()) { + u.setApiKeyAccess(null); + } else { + u.setApiKeyAccess(apiKeyAccess); + } + + AccountVO a = new AccountVO(rs.getLong(7)); + a.setAccountName(rs.getString(8)); + a.setType(Account.Type.getFromValue(rs.getInt(9))); + a.setRoleId(rs.getLong(10)); + a.setDomainId(rs.getLong(11)); + a.setState(State.getValueOf(rs.getString(12))); + apiKeyAccess = rs.getBoolean(13); + if (rs.wasNull()) { + a.setApiKeyAccess(null); + } else { + a.setApiKeyAccess(apiKeyAccess); + } userAcctPair = new Pair(u, a); } diff --git a/engine/schema/src/main/resources/META-INF/db/schema-41910to42000.sql b/engine/schema/src/main/resources/META-INF/db/schema-41910to42000.sql index c36b71c2f250..6e110b5d393b 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-41910to42000.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-41910to42000.sql @@ -425,3 +425,6 @@ INSERT IGNORE INTO `cloud`.`guest_os_hypervisor` (uuid, hypervisor_type, hypervi CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.vm_instance', 'delete_protection', 'boolean DEFAULT FALSE COMMENT "delete protection for vm" '); CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.volumes', 'delete_protection', 'boolean DEFAULT FALSE COMMENT "delete protection for volumes" '); + +CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.user', 'api_key_access', 'boolean DEFAULT NULL COMMENT "is api key access allowed for the user" AFTER `secret_key`'); +CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.account', 'api_key_access', 'boolean DEFAULT NULL COMMENT "is api key access allowed for the account" '); diff --git a/engine/schema/src/main/resources/META-INF/db/views/cloud.account_view.sql b/engine/schema/src/main/resources/META-INF/db/views/cloud.account_view.sql index 87546a9d1188..dc64380fb57b 100644 --- a/engine/schema/src/main/resources/META-INF/db/views/cloud.account_view.sql +++ b/engine/schema/src/main/resources/META-INF/db/views/cloud.account_view.sql @@ -31,6 +31,7 @@ select `account`.`cleanup_needed` AS `cleanup_needed`, `account`.`network_domain` AS `network_domain` , `account`.`default` AS `default`, + `account`.`api_key_access` AS `api_key_access`, `domain`.`id` AS `domain_id`, `domain`.`uuid` AS `domain_uuid`, `domain`.`name` AS `domain_name`, diff --git a/engine/schema/src/main/resources/META-INF/db/views/cloud.user_view.sql b/engine/schema/src/main/resources/META-INF/db/views/cloud.user_view.sql index 7eedc03712b6..340cfa9055fb 100644 --- a/engine/schema/src/main/resources/META-INF/db/views/cloud.user_view.sql +++ b/engine/schema/src/main/resources/META-INF/db/views/cloud.user_view.sql @@ -39,6 +39,7 @@ select user.incorrect_login_attempts, user.source, user.default, + user.api_key_access, account.id account_id, account.uuid account_uuid, account.account_name account_name, diff --git a/framework/config/src/main/java/org/apache/cloudstack/framework/config/ConfigKey.java b/framework/config/src/main/java/org/apache/cloudstack/framework/config/ConfigKey.java index 36a8050754c0..00cf56345c8d 100644 --- a/framework/config/src/main/java/org/apache/cloudstack/framework/config/ConfigKey.java +++ b/framework/config/src/main/java/org/apache/cloudstack/framework/config/ConfigKey.java @@ -34,6 +34,7 @@ public class ConfigKey { public static final String CATEGORY_ADVANCED = "Advanced"; public static final String CATEGORY_ALERT = "Alert"; public static final String CATEGORY_NETWORK = "Network"; + public static final String CATEGORY_SYSTEM = "System"; public enum Scope { Global, Zone, Cluster, StoragePool, Account, ManagementServer, ImageStore, Domain diff --git a/server/src/main/java/com/cloud/api/ApiServer.java b/server/src/main/java/com/cloud/api/ApiServer.java index 739ad765afa0..dc898c17dd46 100644 --- a/server/src/main/java/com/cloud/api/ApiServer.java +++ b/server/src/main/java/com/cloud/api/ApiServer.java @@ -184,6 +184,7 @@ import com.cloud.utils.net.NetUtils; import com.google.gson.reflect.TypeToken; +import static com.cloud.user.AccountManagerImpl.apiKeyAccess; import static org.apache.cloudstack.user.UserPasswordResetManager.UserPasswordResetEnabled; @Component @@ -874,6 +875,34 @@ private void buildAuditTrail(final StringBuilder auditTrailSb, final String comm } } + protected boolean verifyApiKeyAccessAllowed(User user, Account account) { + Boolean apiKeyAccessEnabled = user.getApiKeyAccess(); + if (apiKeyAccessEnabled != null) { + if (apiKeyAccessEnabled == true) { + return true; + } else { + logger.info("Api-Key access is disabled for the User"); + return false; + } + } + apiKeyAccessEnabled = account.getApiKeyAccess(); + if (apiKeyAccessEnabled != null) { + if (apiKeyAccessEnabled == true) { + return true; + } else { + logger.info("Api-Key access is disabled for the Account"); + return false; + } + } + apiKeyAccessEnabled = apiKeyAccess.valueIn(account.getDomainId()); + if (apiKeyAccessEnabled == true) { + return true; + } else { + logger.info("Api-Key access is disabled by the Domain level setting"); + } + return false; + } + @Override public boolean verifyRequest(final Map requestParameters, final Long userId, InetAddress remoteAddress) throws ServerApiException { try { @@ -990,6 +1019,10 @@ public boolean verifyRequest(final Map requestParameters, fina return false; } + if (!verifyApiKeyAccessAllowed(user, account)) { + return false; + } + if (!commandAvailable(remoteAddress, commandName, user)) { return false; } diff --git a/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java b/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java index 4063cfe7a183..2879ef5c1e3c 100644 --- a/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java +++ b/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java @@ -680,8 +680,8 @@ public ListResponse searchForUsers(Long domainId, boolean recursiv Object state = null; String keyword = null; - Pair, Integer> result = getUserListInternal(caller, permittedAccounts, listAll, id, username, type, accountName, state, keyword, domainId, recursive, - null); + Pair, Integer> result = getUserListInternal(caller, permittedAccounts, listAll, id, + username, type, accountName, state, keyword, null, domainId, recursive, null); ListResponse response = new ListResponse(); List userResponses = ViewResponseHelper.createUserResponse(CallContext.current().getCallingAccount().getDomainId(), result.first().toArray(new UserAccountJoinVO[result.first().size()])); @@ -708,6 +708,7 @@ private Pair, Integer> searchForUsersInternal(ListUsersC String accountName = cmd.getAccountName(); Object state = cmd.getState(); String keyword = cmd.getKeyword(); + String apiKeyAccess = cmd.getApiKeyAccess(); Long domainId = cmd.getDomainId(); boolean recursive = cmd.isRecursive(); @@ -716,11 +717,11 @@ private Pair, Integer> searchForUsersInternal(ListUsersC Filter searchFilter = new Filter(UserAccountJoinVO.class, "id", true, startIndex, pageSizeVal); - return getUserListInternal(caller, permittedAccounts, listAll, id, username, type, accountName, state, keyword, domainId, recursive, searchFilter); + return getUserListInternal(caller, permittedAccounts, listAll, id, username, type, accountName, state, keyword, apiKeyAccess, domainId, recursive, searchFilter); } private Pair, Integer> getUserListInternal(Account caller, List permittedAccounts, boolean listAll, Long id, Object username, Object type, - String accountName, Object state, String keyword, Long domainId, boolean recursive, Filter searchFilter) { + String accountName, Object state, String keyword, String apiKeyAccess, Long domainId, boolean recursive, Filter searchFilter) { Ternary domainIdRecursiveListProject = new Ternary(domainId, recursive, null); accountMgr.buildACLSearchParameters(caller, id, accountName, null, permittedAccounts, domainIdRecursiveListProject, listAll, false); domainId = domainIdRecursiveListProject.first(); @@ -746,6 +747,9 @@ private Pair, Integer> getUserListInternal(Account calle sb.and("domainId", sb.entity().getDomainId(), Op.EQ); sb.and("accountName", sb.entity().getAccountName(), Op.EQ); sb.and("state", sb.entity().getState(), Op.EQ); + if (apiKeyAccess != null) { + sb.and("apiKeyAccess", sb.entity().getApiKeyAccess(), Op.EQ); + } if ((accountName == null) && (domainId != null)) { sb.and("domainPath", sb.entity().getDomainPath(), Op.LIKE); @@ -800,6 +804,15 @@ private Pair, Integer> getUserListInternal(Account calle sc.setParameters("state", state); } + if (apiKeyAccess != null) { + try { + ApiConstants.ApiKeyAccess access = ApiConstants.ApiKeyAccess.valueOf(apiKeyAccess.toUpperCase()); + sc.setParameters("apiKeyAccess", access.toBoolean()); + } catch (IllegalArgumentException ex) { + throw new InvalidParameterValueException("ApiKeyAccess value can only be Enabled/Disabled/Inherit"); + } + } + return _userAccountJoinDao.searchAndCount(sc, searchFilter); } @@ -2867,6 +2880,7 @@ private Pair, Integer> searchForAccountIdsAndCount(ListAccountsCmd cm Object state = cmd.getState(); Object isCleanupRequired = cmd.isCleanupRequired(); Object keyword = cmd.getKeyword(); + String apiKeyAccess = cmd.getApiKeyAccess(); SearchBuilder accountSearchBuilder = _accountDao.createSearchBuilder(); accountSearchBuilder.select(null, Func.DISTINCT, accountSearchBuilder.entity().getId()); // select distinct @@ -2879,6 +2893,9 @@ private Pair, Integer> searchForAccountIdsAndCount(ListAccountsCmd cm accountSearchBuilder.and("typeNEQ", accountSearchBuilder.entity().getType(), SearchCriteria.Op.NEQ); accountSearchBuilder.and("idNEQ", accountSearchBuilder.entity().getId(), SearchCriteria.Op.NEQ); accountSearchBuilder.and("type2NEQ", accountSearchBuilder.entity().getType(), SearchCriteria.Op.NEQ); + if (apiKeyAccess != null) { + accountSearchBuilder.and("apiKeyAccess", accountSearchBuilder.entity().getApiKeyAccess(), Op.EQ); + } if (domainId != null && isRecursive) { SearchBuilder domainSearch = _domainDao.createSearchBuilder(); @@ -2942,6 +2959,15 @@ private Pair, Integer> searchForAccountIdsAndCount(ListAccountsCmd cm } } + if (apiKeyAccess != null) { + try { + ApiConstants.ApiKeyAccess access = ApiConstants.ApiKeyAccess.valueOf(apiKeyAccess.toUpperCase()); + sc.setParameters("apiKeyAccess", access.toBoolean()); + } catch (IllegalArgumentException ex) { + throw new InvalidParameterValueException("ApiKeyAccess value can only be Enabled/Disabled/Inherit"); + } + } + Pair, Integer> uniqueAccountPair = _accountDao.searchAndCount(sc, searchFilter); Integer count = uniqueAccountPair.second(); List accountIds = uniqueAccountPair.first().stream().map(AccountVO::getId).collect(Collectors.toList()); diff --git a/server/src/main/java/com/cloud/api/query/dao/AccountJoinDaoImpl.java b/server/src/main/java/com/cloud/api/query/dao/AccountJoinDaoImpl.java index 7ffd3ef319fe..623188deb7e9 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,7 @@ public AccountResponse newAccountResponse(ResponseView view, EnumSet 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(); } @@ -1449,6 +1456,7 @@ public UserAccount updateUser(UpdateUserCmd updateUserCmd) { logger.debug("Updating user with Id: " + user.getUuid()); validateAndUpdateApiAndSecretKeyIfNeeded(updateUserCmd, user); + validateAndUpdateUserApiKeyAccess(updateUserCmd, user); Account account = retrieveAndValidateAccount(user); validateAndUpdateFirstNameIfNeeded(updateUserCmd, user); @@ -1668,6 +1676,28 @@ protected void validateAndUpdateApiAndSecretKeyIfNeeded(UpdateUserCmd updateUser user.setSecretKey(secretKey); } + protected void validateAndUpdateUserApiKeyAccess(UpdateUserCmd updateUserCmd, UserVO user) { + if (updateUserCmd.getApiKeyAccess() != null) { + try { + ApiConstants.ApiKeyAccess access = ApiConstants.ApiKeyAccess.valueOf(updateUserCmd.getApiKeyAccess().toUpperCase()); + user.setApiKeyAccess(access.toBoolean()); + } 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()); + } 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}. */ @@ -2034,6 +2064,8 @@ public AccountVO updateAccount(UpdateAccountCmd cmd) { Account caller = getCurrentCallingAccount(); checkAccess(caller, _domainMgr.getDomain(account.getDomainId())); + validateAndUpdateAccountApiKeyAccess(cmd, acctForUpdate); + if(newAccountName != null) { if (newAccountName.isEmpty()) { @@ -3306,7 +3338,7 @@ public String getConfigComponentName() { @Override public ConfigKey[] getConfigKeys() { return new ConfigKey[] {UseSecretKeyInResponse, enableUserTwoFactorAuthentication, - userTwoFactorAuthenticationDefaultProvider, mandateUserTwoFactorAuthentication, userTwoFactorAuthenticationIssuer}; + userTwoFactorAuthenticationDefaultProvider, mandateUserTwoFactorAuthentication, userTwoFactorAuthenticationIssuer, apiKeyAccess}; } public List getUserTwoFactorAuthenticationProviders() { diff --git a/server/src/test/java/com/cloud/api/ApiServerTest.java b/server/src/test/java/com/cloud/api/ApiServerTest.java index fed1d95a625e..dedd6e02ec5c 100644 --- a/server/src/test/java/com/cloud/api/ApiServerTest.java +++ b/server/src/test/java/com/cloud/api/ApiServerTest.java @@ -17,6 +17,8 @@ package com.cloud.api; import com.cloud.domain.Domain; +import com.cloud.user.Account; +import com.cloud.user.User; import com.cloud.user.UserAccount; import com.cloud.utils.exception.CloudRuntimeException; import org.apache.cloudstack.framework.config.ConfigKey; @@ -147,4 +149,31 @@ public void testForgotPasswordFailureInactiveDomain() { Mockito.when(domain.getState()).thenReturn(Domain.State.Inactive); apiServer.forgotPassword(userAccount, domain); } + + @Test + public void testVerifyApiKeyAccessAllowed() { + Long domainId = 1L; + User user = Mockito.mock(User.class); + Account account = Mockito.mock(Account.class); + + Mockito.when(user.getApiKeyAccess()).thenReturn(true); + Assert.assertEquals(true, apiServer.verifyApiKeyAccessAllowed(user, account)); + Mockito.verify(account, Mockito.never()).getApiKeyAccess(); + + Mockito.when(user.getApiKeyAccess()).thenReturn(false); + Assert.assertEquals(false, apiServer.verifyApiKeyAccessAllowed(user, account)); + Mockito.verify(account, Mockito.never()).getApiKeyAccess(); + + Mockito.when(user.getApiKeyAccess()).thenReturn(null); + Mockito.when(account.getApiKeyAccess()).thenReturn(true); + Assert.assertEquals(true, apiServer.verifyApiKeyAccessAllowed(user, account)); + + Mockito.when(user.getApiKeyAccess()).thenReturn(null); + Mockito.when(account.getApiKeyAccess()).thenReturn(false); + Assert.assertEquals(false, apiServer.verifyApiKeyAccessAllowed(user, account)); + + Mockito.when(user.getApiKeyAccess()).thenReturn(null); + Mockito.when(account.getApiKeyAccess()).thenReturn(null); + Assert.assertEquals(true, apiServer.verifyApiKeyAccessAllowed(user, account)); + } } diff --git a/server/src/test/java/com/cloud/api/query/QueryManagerImplTest.java b/server/src/test/java/com/cloud/api/query/QueryManagerImplTest.java index f5de105e22c4..0926df00c1ef 100644 --- a/server/src/test/java/com/cloud/api/query/QueryManagerImplTest.java +++ b/server/src/test/java/com/cloud/api/query/QueryManagerImplTest.java @@ -17,13 +17,18 @@ package com.cloud.api.query; +import com.cloud.api.ApiDBUtils; import com.cloud.api.query.dao.TemplateJoinDao; +import com.cloud.api.query.dao.UserAccountJoinDao; import com.cloud.api.query.dao.UserVmJoinDao; import com.cloud.api.query.vo.EventJoinVO; import com.cloud.api.query.vo.TemplateJoinVO; +import com.cloud.api.query.vo.UserAccountJoinVO; import com.cloud.api.query.vo.UserVmJoinVO; import com.cloud.dc.ClusterVO; import com.cloud.dc.dao.ClusterDao; +import com.cloud.domain.DomainVO; +import com.cloud.domain.dao.DomainDao; import com.cloud.event.EventVO; import com.cloud.event.dao.EventDao; import com.cloud.event.dao.EventJoinDao; @@ -45,6 +50,7 @@ import com.cloud.user.AccountVO; import com.cloud.user.User; import com.cloud.user.UserVO; +import com.cloud.user.dao.AccountDao; import com.cloud.utils.Pair; import com.cloud.utils.db.EntityManager; import com.cloud.utils.db.Filter; @@ -57,7 +63,9 @@ import org.apache.cloudstack.acl.SecurityChecker; import org.apache.cloudstack.api.ApiCommandResourceType; 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 +73,7 @@ import org.apache.cloudstack.api.response.EventResponse; import org.apache.cloudstack.api.response.ListResponse; import org.apache.cloudstack.api.response.ObjectStoreResponse; +import org.apache.cloudstack.api.response.UserResponse; import org.apache.cloudstack.api.response.VirtualMachineResponse; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.storage.datastore.db.ObjectStoreDao; @@ -150,6 +159,15 @@ public class QueryManagerImplTest { @Mock UserVmJoinDao userVmJoinDao; + @Mock + UserAccountJoinDao userAccountJoinDao; + + @Mock + DomainDao domainDao; + + @Mock + AccountDao accountDao; + private AccountVO account; private UserVO user; @@ -477,4 +495,79 @@ public void testListAffectedVmsForScopeChange() { Assert.assertEquals(response.getResponses().get(0).getId(), instanceUuid); Assert.assertEquals(response.getResponses().get(0).getName(), vmName); } + + @Test + public void testSearchForUsers() { + ListUsersCmd cmd = Mockito.mock(ListUsersCmd.class); + String username = "Admin"; + String accountName = "Admin"; + Account.Type accountType = Account.Type.ADMIN; + Long domainId = 1L; + String apiKeyAccess = "Disabled"; + Mockito.when(cmd.getUsername()).thenReturn(username); + Mockito.when(cmd.getAccountName()).thenReturn(accountName); + Mockito.when(cmd.getAccountType()).thenReturn(accountType); + Mockito.when(cmd.getDomainId()).thenReturn(domainId); + Mockito.when(cmd.getApiKeyAccess()).thenReturn(apiKeyAccess); + + UserAccountJoinVO user = new UserAccountJoinVO(); + DomainVO domain = Mockito.mock(DomainVO.class); + SearchBuilder sb = Mockito.mock(SearchBuilder.class); + SearchCriteria sc = Mockito.mock(SearchCriteria.class); + List users = new ArrayList<>(); + Pair, Integer> result = new Pair<>(users, 0); + UserResponse response = Mockito.mock(UserResponse.class); + + Mockito.when(userAccountJoinDao.createSearchBuilder()).thenReturn(sb); + Mockito.when(sb.entity()).thenReturn(user); + Mockito.when(sb.create()).thenReturn(sc); + Mockito.when(userAccountJoinDao.searchAndCount(any(SearchCriteria.class), any(Filter.class))).thenReturn(result); + + queryManager.searchForUsers(cmd); + + Mockito.verify(sc).setParameters("username", username); + Mockito.verify(sc).setParameters("accountName", accountName); + Mockito.verify(sc).setParameters("type", accountType); + Mockito.verify(sc).setParameters("domainId", domainId); + Mockito.verify(sc).setParameters("apiKeyAccess", false); + Mockito.verify(userAccountJoinDao, Mockito.times(1)).searchAndCount( + any(SearchCriteria.class), any(Filter.class)); + } + + @Test + public void testSearchForAccounts() { + ListAccountsCmd cmd = Mockito.mock(ListAccountsCmd.class); + Long domainId = 1L; + String accountName = "Admin"; + Account.Type accountType = Account.Type.ADMIN; + String apiKeyAccess = "Enabled"; + Mockito.when(cmd.getId()).thenReturn(null); + Mockito.when(cmd.getDomainId()).thenReturn(domainId); + Mockito.when(cmd.getSearchName()).thenReturn(accountName); + Mockito.when(cmd.getAccountType()).thenReturn(accountType); + Mockito.when(cmd.getApiKeyAccess()).thenReturn(apiKeyAccess); + + DomainVO domain = Mockito.mock(DomainVO.class); + SearchBuilder sb = Mockito.mock(SearchBuilder.class); + SearchCriteria sc = Mockito.mock(SearchCriteria.class); + Pair, Integer> uniqueAccountPair = new Pair<>(new ArrayList<>(), 0); + Mockito.when(domainDao.findById(domainId)).thenReturn(domain); + Mockito.doNothing().when(accountManager).checkAccess(account, domain); + + Mockito.when(accountDao.createSearchBuilder()).thenReturn(sb); + Mockito.when(sb.entity()).thenReturn(account); + Mockito.when(sb.create()).thenReturn(sc); + Mockito.when(accountDao.searchAndCount(any(SearchCriteria.class), any(Filter.class))).thenReturn(uniqueAccountPair); + + try (MockedStatic apiDBUtilsMocked = Mockito.mockStatic(ApiDBUtils.class)) { + queryManager.searchForAccounts(cmd); + } + + Mockito.verify(sc).setParameters("domainId", domainId); + Mockito.verify(sc).setParameters("accountName", accountName); + Mockito.verify(sc).setParameters("type", accountType); + Mockito.verify(sc).setParameters("apiKeyAccess", true); + Mockito.verify(accountDao, Mockito.times(1)).searchAndCount( + any(SearchCriteria.class), any(Filter.class)); + } } diff --git a/server/src/test/java/com/cloud/user/AccountManagerImplTest.java b/server/src/test/java/com/cloud/user/AccountManagerImplTest.java index 9daa19206faa..f6b775a7b368 100644 --- a/server/src/test/java/com/cloud/user/AccountManagerImplTest.java +++ b/server/src/test/java/com/cloud/user/AccountManagerImplTest.java @@ -27,6 +27,7 @@ import java.util.Map; 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 +91,9 @@ public class AccountManagerImplTest extends AccountManagetImplTestBase { @Mock private UpdateUserCmd UpdateUserCmdMock; + @Mock + private UpdateAccountCmd UpdateAccountCmdMock; + private long userVoIdMock = 111l; @Mock private UserVO userVoMock; @@ -507,6 +511,34 @@ public void validateAndUpdatApiAndSecretKeyIfNeededTest() { Mockito.verify(userVoMock).setSecretKey(secretKey); } + @Test + public void validateAndUpdatUserApiKeyAccess() { + Mockito.doReturn("Enabled").when(UpdateUserCmdMock).getApiKeyAccess(); + 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(); + 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/ui/public/locales/en.json b/ui/public/locales/en.json index eb1be420b2ed..2ec973711a4b 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", @@ -3539,6 +3541,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/SearchView.vue b/ui/src/components/view/SearchView.vue index c82b1172f2d6..786a9f15be60 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 28c0e3f556d6..21996167705d 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 d8c4ac06d0c0..afd895284aa6 100644 --- a/ui/src/config/section/user.js +++ b/ui/src/config/section/user.js @@ -25,8 +25,15 @@ 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'], + details: ['username', 'id', 'firstname', 'lastname', 'email', 'usersource', 'timezone', 'rolename', 'roletype', 'is2faenabled', 'apikeyaccess', 'account', 'domain', 'created'], tabs: [ { name: 'details', diff --git a/ui/src/views/iam/EditAccount.vue b/ui/src/views/iam/EditAccount.vue new file mode 100644 index 000000000000..6c9c2b9e496f --- /dev/null +++ b/ui/src/views/iam/EditAccount.vue @@ -0,0 +1,195 @@ +// 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. + + + + diff --git a/ui/src/views/iam/EditUser.vue b/ui/src/views/iam/EditUser.vue index e082fd17a06f..3d6e293756f7 100644 --- a/ui/src/views/iam/EditUser.vue +++ b/ui/src/views/iam/EditUser.vue @@ -81,6 +81,21 @@ + + + + Disabled + Enabled + Inherit + +
{{ $t('label.cancel') }} {{ $t('label.ok') }} @@ -128,6 +143,11 @@ export default { this.initForm() this.fetchData() }, + computed: { + isRootAdmin () { + return this.$store.getters.userInfo?.roletype === 'Admin' + } + }, methods: { initForm () { this.formRef = ref() @@ -187,7 +207,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