From 6d8fc4766b60a7ba1881433d285b94998c029a5e Mon Sep 17 00:00:00 2001 From: Nicole Schmidt Date: Tue, 14 Jan 2025 08:18:44 -0300 Subject: [PATCH 01/17] Update api keypair based on upstream recent fixes --- .../main/java/com/cloud/event/EventTypes.java | 3 +- .../java/com/cloud/user/AccountService.java | 33 +- .../java/com/cloud/user/ApiKeyPairState.java | 21 + api/src/main/java/com/cloud/user/User.java | 8 - .../main/java/com/cloud/user/UserAccount.java | 4 - .../org/apache/cloudstack/acl/APIChecker.java | 5 +- .../apache/cloudstack/acl/RoleService.java | 21 + .../java/org/apache/cloudstack/acl/Rule.java | 6 +- .../cloudstack/acl/apikeypair/ApiKeyPair.java | 38 ++ .../acl/apikeypair/ApiKeyPairPermission.java | 23 + .../acl/apikeypair/ApiKeyPairService.java | 29 + .../apache/cloudstack/api/ApiConstants.java | 10 +- .../apache/cloudstack/api/BaseAsyncCmd.java | 2 + .../org/apache/cloudstack/api/BaseCmd.java | 3 + .../cloudstack/api/ResponseGenerator.java | 8 + .../admin/storage/AddImageStoreS3CMD.java | 6 +- .../command/admin/user/DeleteUserKeysCmd.java | 79 +++ .../command/admin/user/GetUserKeysCmd.java | 32 +- .../admin/user/ListUserKeyRulesCmd.java | 70 +++ .../command/admin/user/ListUserKeysCmd.java | 95 ++++ .../api/command/admin/user/RegisterCmd.java | 93 ---- .../admin/user/RegisterUserKeysCmd.java | 205 +++++++ .../api/command/admin/user/UpdateUserCmd.java | 10 +- .../api/response/ApiKeyPairResponse.java | 285 ++++++++++ .../cloudstack/api/response/UserResponse.java | 19 +- .../apache/cloudstack/query/QueryService.java | 2 + .../org/apache/cloudstack/acl/RuleTest.java | 141 ++++- .../cloud/upgrade/dao/Upgrade410to420.java | 2 +- .../upgrade/dao/Upgrade42010to42100.java | 50 +- .../java/com/cloud/user/UserAccountVO.java | 26 - .../src/main/java/com/cloud/user/UserVO.java | 30 - .../java/com/cloud/user/dao/AccountDao.java | 4 +- .../com/cloud/user/dao/AccountDaoImpl.java | 65 ++- .../com/cloud/user/dao/UserAccountDao.java | 2 - .../cloud/user/dao/UserAccountDaoImpl.java | 17 - .../main/java/com/cloud/user/dao/UserDao.java | 7 - .../java/com/cloud/user/dao/UserDaoImpl.java | 11 - .../acl/ApiKeyPairPermissionVO.java | 57 ++ .../apache/cloudstack/acl/ApiKeyPairVO.java | 244 +++++++++ .../cloudstack/acl/dao/ApiKeyPairDao.java | 38 ++ .../cloudstack/acl/dao/ApiKeyPairDaoImpl.java | 117 ++++ .../acl/dao/ApiKeyPairPermissionsDao.java | 28 + .../acl/dao/ApiKeyPairPermissionsDaoImpl.java | 67 +++ .../db/ImageStoreDetailsDaoImpl.java | 2 +- ...spring-engine-schema-core-daos-context.xml | 2 + .../META-INF/db/schema-42010to42100.sql | 34 ++ .../META-INF/db/views/cloud.user_view.sql | 2 - .../image/datastore/ImageStoreHelper.java | 2 +- .../acl/DynamicRoleBasedAPIAccessChecker.java | 43 +- .../DynamicRoleBasedAPIAccessCheckerTest.java | 66 +++ .../acl/ProjectRoleBasedApiAccessChecker.java | 5 +- .../acl/StaticRoleBasedAPIAccessChecker.java | 5 +- plugins/api/discovery/pom.xml | 6 + .../command/user/discovery/ListApisCmd.java | 2 +- .../discovery/ApiDiscoveryService.java | 3 +- .../discovery/ApiDiscoveryServiceImpl.java | 71 ++- .../discovery/ApiDiscoveryTest.java | 10 +- .../ratelimit/ApiRateLimitServiceImpl.java | 7 +- .../response/QuotaResponseBuilderImpl.java | 2 +- .../QuotaResponseBuilderImplTest.java | 6 +- .../manager/BaremetalVlanManagerImpl.java | 5 +- .../cluster/KubernetesClusterManagerImpl.java | 9 +- .../management/MockAccountManager.java | 57 +- .../tungsten/service/TungstenServiceImpl.java | 9 +- .../tungsten/service/TungstenElementTest.java | 5 + .../driver/S3ImageStoreDriverImpl.java | 2 +- .../main/java/com/cloud/api/ApiDBUtils.java | 17 +- .../java/com/cloud/api/ApiResponseHelper.java | 94 ++++ .../main/java/com/cloud/api/ApiServer.java | 59 +- .../main/java/com/cloud/api/ApiServlet.java | 4 +- .../com/cloud/api/query/QueryManagerImpl.java | 37 +- .../api/query/dao/UserAccountJoinDao.java | 3 +- .../api/query/dao/UserAccountJoinDaoImpl.java | 9 +- .../cloud/api/query/vo/UserAccountJoinVO.java | 16 - .../network/as/AutoScaleManagerImpl.java | 12 +- .../lb/LoadBalancingRulesManagerImpl.java | 16 +- .../VirtualNetworkApplianceManagerImpl.java | 12 +- .../cloud/server/ManagementServerImpl.java | 22 +- .../cloud/servlet/ConsoleProxyServlet.java | 16 +- .../java/com/cloud/user/AccountManager.java | 4 +- .../com/cloud/user/AccountManagerImpl.java | 515 +++++++++++++++--- .../cloudstack/acl/ApiKeyPairManagerImpl.java | 83 +++ .../cloudstack/acl/RoleManagerImpl.java | 68 +-- .../spring-server-core-managers-context.xml | 2 + .../network/as/AutoScaleManagerImplTest.java | 42 +- .../cloud/user/AccountManagerImplTest.java | 417 +++++++++++++- .../cloud/user/MockAccountManagerImpl.java | 64 ++- .../cloudstack/acl/RoleManagerImplTest.java | 32 +- tools/marvin/marvin/cloudstackTestClient.py | 22 +- tools/marvin/marvin/lib/base.py | 1 + 90 files changed, 3332 insertions(+), 606 deletions(-) create mode 100644 api/src/main/java/com/cloud/user/ApiKeyPairState.java create mode 100644 api/src/main/java/org/apache/cloudstack/acl/apikeypair/ApiKeyPair.java create mode 100644 api/src/main/java/org/apache/cloudstack/acl/apikeypair/ApiKeyPairPermission.java create mode 100644 api/src/main/java/org/apache/cloudstack/acl/apikeypair/ApiKeyPairService.java create mode 100644 api/src/main/java/org/apache/cloudstack/api/command/admin/user/DeleteUserKeysCmd.java create mode 100644 api/src/main/java/org/apache/cloudstack/api/command/admin/user/ListUserKeyRulesCmd.java create mode 100644 api/src/main/java/org/apache/cloudstack/api/command/admin/user/ListUserKeysCmd.java delete mode 100644 api/src/main/java/org/apache/cloudstack/api/command/admin/user/RegisterCmd.java create mode 100644 api/src/main/java/org/apache/cloudstack/api/command/admin/user/RegisterUserKeysCmd.java create mode 100644 api/src/main/java/org/apache/cloudstack/api/response/ApiKeyPairResponse.java create mode 100644 engine/schema/src/main/java/org/apache/cloudstack/acl/ApiKeyPairPermissionVO.java create mode 100644 engine/schema/src/main/java/org/apache/cloudstack/acl/ApiKeyPairVO.java create mode 100644 engine/schema/src/main/java/org/apache/cloudstack/acl/dao/ApiKeyPairDao.java create mode 100644 engine/schema/src/main/java/org/apache/cloudstack/acl/dao/ApiKeyPairDaoImpl.java create mode 100644 engine/schema/src/main/java/org/apache/cloudstack/acl/dao/ApiKeyPairPermissionsDao.java create mode 100644 engine/schema/src/main/java/org/apache/cloudstack/acl/dao/ApiKeyPairPermissionsDaoImpl.java create mode 100644 server/src/main/java/org/apache/cloudstack/acl/ApiKeyPairManagerImpl.java diff --git a/api/src/main/java/com/cloud/event/EventTypes.java b/api/src/main/java/com/cloud/event/EventTypes.java index e68da0f51821..8441188d6e06 100644 --- a/api/src/main/java/com/cloud/event/EventTypes.java +++ b/api/src/main/java/com/cloud/event/EventTypes.java @@ -290,8 +290,9 @@ public class EventTypes { //registering userdata events public static final String EVENT_REGISTER_USER_DATA = "REGISTER.USER.DATA"; - //register for user API and secret keys + //user API and secret keys public static final String EVENT_REGISTER_FOR_SECRET_API_KEY = "REGISTER.USER.KEY"; + public static final String EVENT_DELETE_SECRET_API_KEY = "DELETE.USER.KEY"; public static final String API_KEY_ACCESS_UPDATE = "API.KEY.ACCESS.UPDATE"; // Template Events diff --git a/api/src/main/java/com/cloud/user/AccountService.java b/api/src/main/java/com/cloud/user/AccountService.java index e2c3bed0c295..343b3b579d81 100644 --- a/api/src/main/java/com/cloud/user/AccountService.java +++ b/api/src/main/java/com/cloud/user/AccountService.java @@ -23,10 +23,10 @@ import org.apache.cloudstack.acl.ControlledEntity; import org.apache.cloudstack.acl.RoleType; import org.apache.cloudstack.acl.SecurityChecker.AccessType; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPair; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPairPermission; +import org.apache.cloudstack.api.BaseCmd; import org.apache.cloudstack.api.command.admin.account.CreateAccountCmd; -import org.apache.cloudstack.api.command.admin.user.GetUserKeysCmd; -import org.apache.cloudstack.api.command.admin.user.RegisterCmd; -import org.apache.cloudstack.api.command.admin.user.UpdateUserCmd; import com.cloud.dc.DataCenter; import com.cloud.domain.Domain; @@ -35,6 +35,14 @@ import com.cloud.offering.DiskOffering; import com.cloud.offering.NetworkOffering; import com.cloud.offering.ServiceOffering; +import org.apache.cloudstack.api.command.admin.user.DeleteUserKeysCmd; +import org.apache.cloudstack.api.command.admin.user.GetUserKeysCmd; +import org.apache.cloudstack.api.command.admin.user.ListUserKeyRulesCmd; +import org.apache.cloudstack.api.command.admin.user.ListUserKeysCmd; +import org.apache.cloudstack.api.command.admin.user.RegisterUserKeysCmd; +import org.apache.cloudstack.api.command.admin.user.UpdateUserCmd; +import org.apache.cloudstack.api.response.ApiKeyPairResponse; +import org.apache.cloudstack.api.response.ListResponse; import org.apache.cloudstack.auth.UserTwoFactorAuthenticator; public interface AccountService { @@ -93,7 +101,7 @@ User createUser(String userName, String password, String firstName, String lastN void markUserRegistered(long userId); - public String[] createApiKeyAndSecretKey(RegisterCmd cmd); + ApiKeyPair createApiKeyAndSecretKey(RegisterUserKeysCmd cmd); public String[] createApiKeyAndSecretKey(final long userId); @@ -119,6 +127,8 @@ User createUser(String userName, String password, String firstName, String lastN void validateAccountHasAccessToResource(Account account, AccessType accessType, Object resource); + void validateCallingUserHasAccessToDesiredUser(Long userId); + Long finalyzeAccountId(String accountName, Long domainId, Long projectId, boolean enabledOnly); /** @@ -130,7 +140,13 @@ User createUser(String userName, String password, String firstName, String lastN public Pair> getKeys(GetUserKeysCmd cmd); - public Pair> getKeys(Long userId); + ListResponse listKeys(ListUserKeysCmd cmd); + + List listKeyRules(ListUserKeyRulesCmd cmd); + + void deleteApiKey(DeleteUserKeysCmd cmd); + + void deleteApiKey(ApiKeyPair id); /** * Lists user two-factor authentication provider plugins @@ -145,4 +161,11 @@ User createUser(String userName, String password, String firstName, String lastN */ UserTwoFactorAuthenticator getUserTwoFactorAuthenticationProvider(final Long domainId); + ApiKeyPair getLatestUserKeyPair(Long userId); + + ApiKeyPair getKeyPairById(Long id); + + ApiKeyPair getKeyPairByApiKey(String apiKey); + + String getAccessingApiKey(BaseCmd cmd); } diff --git a/api/src/main/java/com/cloud/user/ApiKeyPairState.java b/api/src/main/java/com/cloud/user/ApiKeyPairState.java new file mode 100644 index 000000000000..63405c62e320 --- /dev/null +++ b/api/src/main/java/com/cloud/user/ApiKeyPairState.java @@ -0,0 +1,21 @@ +// 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.user; + +public enum ApiKeyPairState { + ENABLED, REMOVED, EXPIRED +} diff --git a/api/src/main/java/com/cloud/user/User.java b/api/src/main/java/com/cloud/user/User.java index 041b39ad2729..da7245a47980 100644 --- a/api/src/main/java/com/cloud/user/User.java +++ b/api/src/main/java/com/cloud/user/User.java @@ -65,14 +65,6 @@ public enum Source { public void setState(Account.State state); - public String getApiKey(); - - public void setApiKey(String apiKey); - - public String getSecretKey(); - - public void setSecretKey(String secretKey); - public String getTimezone(); public void setTimezone(String timezone); diff --git a/api/src/main/java/com/cloud/user/UserAccount.java b/api/src/main/java/com/cloud/user/UserAccount.java index e6b07fb371eb..5736244e3259 100644 --- a/api/src/main/java/com/cloud/user/UserAccount.java +++ b/api/src/main/java/com/cloud/user/UserAccount.java @@ -39,10 +39,6 @@ public interface UserAccount extends InternalIdentity { String getState(); - String getApiKey(); - - String getSecretKey(); - Date getCreated(); Date getRemoved(); diff --git a/api/src/main/java/org/apache/cloudstack/acl/APIChecker.java b/api/src/main/java/org/apache/cloudstack/acl/APIChecker.java index 660f64f43ef2..ea08b0860565 100644 --- a/api/src/main/java/org/apache/cloudstack/acl/APIChecker.java +++ b/api/src/main/java/org/apache/cloudstack/acl/APIChecker.java @@ -20,6 +20,7 @@ import com.cloud.user.Account; import com.cloud.user.User; import com.cloud.utils.component.Adapter; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPairPermission; import java.util.List; @@ -31,8 +32,8 @@ public interface APIChecker extends Adapter { // If true, apiChecker has checked the operation // If false, apiChecker is unable to handle the operation or not implemented // On exception, checkAccess failed don't allow - boolean checkAccess(User user, String apiCommandName) throws PermissionDeniedException; - boolean checkAccess(Account account, String apiCommandName) throws PermissionDeniedException; + boolean checkAccess(User user, String apiCommandName, ApiKeyPairPermission... apiKeyPairPermissions) throws PermissionDeniedException; + boolean checkAccess(Account account, String apiCommandName, ApiKeyPairPermission... apiKeyPairPermissions) throws PermissionDeniedException; /** * Verifies if the account has permission for the given list of APIs and returns only the allowed ones. * diff --git a/api/src/main/java/org/apache/cloudstack/acl/RoleService.java b/api/src/main/java/org/apache/cloudstack/acl/RoleService.java index f041c8342aec..8e06864b5ede 100644 --- a/api/src/main/java/org/apache/cloudstack/acl/RoleService.java +++ b/api/src/main/java/org/apache/cloudstack/acl/RoleService.java @@ -104,5 +104,26 @@ public interface RoleService { List findAllPermissionsBy(Long roleId); + List findAllRolePermissionsEntityBy(Long roleId); + Permission getRolePermission(String permission); + + int removeRolesIfNeeded(List roles); + + /** + * Checks if the role of the caller account has compatible permissions of the specified role permissions. + * For each permission of the roleToAccess, the role of the caller needs to contain the same permission. + * + * @param rolePermissions the permissions of the caller role. + * @param rolePermissionsToAccess the permissions for the role that the caller role wants to access. + * @return True if the role can be accessed with the given permissions; false otherwise. + */ + boolean roleHasPermission(Map rolePermissions, List rolePermissionsToAccess); + + /** + * Given a list of role permissions, returns a {@link Map} containing the API name as the key and the {@link Permission} for the API as the value. + * + * @param rolePermissions Permissions for the role from role. + */ + Map getRoleRulesAndPermissions(List rolePermissions); } diff --git a/api/src/main/java/org/apache/cloudstack/acl/Rule.java b/api/src/main/java/org/apache/cloudstack/acl/Rule.java index a4ef7773f67b..ad01825a95f1 100644 --- a/api/src/main/java/org/apache/cloudstack/acl/Rule.java +++ b/api/src/main/java/org/apache/cloudstack/acl/Rule.java @@ -25,16 +25,18 @@ public final class Rule { private final String rule; + private final Pattern matchingPattern; private final static Pattern ALLOWED_PATTERN = Pattern.compile("^[a-zA-Z0-9*]+$"); public Rule(final String rule) { validate(rule); this.rule = rule; + matchingPattern = Pattern.compile(rule.toLowerCase().replace("*", "(\\w*\\*?)+")); } public boolean matches(final String commandName) { - return StringUtils.isNotEmpty(commandName) - && commandName.toLowerCase().matches(rule.toLowerCase().replace("*", "\\w*")); + return StringUtils.isNotEmpty(commandName) && + matchingPattern.matcher(commandName.toLowerCase()).matches(); } public String getRuleString() { diff --git a/api/src/main/java/org/apache/cloudstack/acl/apikeypair/ApiKeyPair.java b/api/src/main/java/org/apache/cloudstack/acl/apikeypair/ApiKeyPair.java new file mode 100644 index 000000000000..ecce0ae50824 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/acl/apikeypair/ApiKeyPair.java @@ -0,0 +1,38 @@ +// 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 org.apache.cloudstack.acl.apikeypair; + +import org.apache.cloudstack.acl.ControlledEntity; +import org.apache.cloudstack.api.Identity; +import org.apache.cloudstack.api.InternalIdentity; + +import java.util.Date; + +public interface ApiKeyPair extends ControlledEntity, InternalIdentity, Identity { + Long getUserId(); + Date getStartDate(); + Date getEndDate(); + Date getCreated(); + String getDescription(); + String getApiKey(); + String getSecretKey(); + String getName(); + Date getRemoved(); + void setRemoved(Date date); + void validateDate(); + boolean hasEndDatePassed(); +} diff --git a/api/src/main/java/org/apache/cloudstack/acl/apikeypair/ApiKeyPairPermission.java b/api/src/main/java/org/apache/cloudstack/acl/apikeypair/ApiKeyPairPermission.java new file mode 100644 index 000000000000..60b3834cc073 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/acl/apikeypair/ApiKeyPairPermission.java @@ -0,0 +1,23 @@ +// 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 org.apache.cloudstack.acl.apikeypair; + +import org.apache.cloudstack.acl.RolePermissionEntity; + +public interface ApiKeyPairPermission extends RolePermissionEntity { + long getApiKeyPairId(); +} diff --git a/api/src/main/java/org/apache/cloudstack/acl/apikeypair/ApiKeyPairService.java b/api/src/main/java/org/apache/cloudstack/acl/apikeypair/ApiKeyPairService.java new file mode 100644 index 000000000000..d2eb3bc6ce25 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/acl/apikeypair/ApiKeyPairService.java @@ -0,0 +1,29 @@ +// 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 org.apache.cloudstack.acl.apikeypair; + +import java.util.List; + +public interface ApiKeyPairService { + List findAllPermissionsByKeyPairId(Long apiKeyPairId, Long roleId); + + ApiKeyPair findByApiKey(String apiKey); + + ApiKeyPair findById(Long id); + + void validateCallingUserHasAccessToDesiredUser(Long userId); +} 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 aefcfb0d7d16..643ae717158e 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -19,6 +19,7 @@ public class ApiConstants { public static final String ACCOUNT = "account"; public static final String ACCOUNTS = "accounts"; + public static final String ACCOUNT_NAME = "accountname"; public static final String ACCOUNT_TYPE = "accounttype"; public static final String ACCOUNT_ID = "accountid"; public static final String ACCOUNT_IDS = "accountids"; @@ -42,6 +43,7 @@ public class ApiConstants { public static final String AS_NUMBER_ID = "asnumberid"; public static final String ASN_RANGE = "asnrange"; public static final String ASN_RANGE_ID = "asnrangeid"; + public static final String API_KEY_FILTER = "apikeyfilter"; public static final String ASYNC_BACKUP = "asyncbackup"; public static final String AUTO_SELECT = "autoselect"; public static final String USER_API_KEY = "userapikey"; @@ -311,6 +313,7 @@ public class ApiConstants { public static final String JOB_STATUS = "jobstatus"; public static final String KEEPALIVE_ENABLED = "keepaliveenabled"; public static final String KERNEL_VERSION = "kernelversion"; + public static final String KEYPAIR_ID = "keypairid"; public static final String KEY = "key"; public static final String LABEL = "label"; public static final String LASTNAME = "lastname"; @@ -446,9 +449,9 @@ public class ApiConstants { public static final String SCHEDULE = "schedule"; public static final String SCHEDULE_ID = "scheduleid"; public static final String SCOPE = "scope"; + public static final String USER_SECRET_KEY = "usersecretkey"; public static final String SEARCH_BASE = "searchbase"; public static final String SECONDARY_IP = "secondaryip"; - public static final String SECRET_KEY = "secretkey"; public static final String SECURITY_GROUP_IDS = "securitygroupids"; public static final String SECURITY_GROUP_NAMES = "securitygroupnames"; public static final String SECURITY_GROUP_NAME = "securitygroupname"; @@ -464,6 +467,7 @@ public class ApiConstants { public static final String SHOW_RESOURCE_ICON = "showicon"; public static final String SHOW_INACTIVE = "showinactive"; public static final String SHOW_UNIQUE = "showunique"; + public static final String SHOW_PERMISSIONS = "showpermissions"; public static final String SIGNATURE = "signature"; public static final String SIGNATURE_VERSION = "signatureversion"; public static final String SINCE = "since"; @@ -539,7 +543,6 @@ public class ApiConstants { public static final String USERNAME = "username"; public static final String USER_CONFIGURABLE = "userconfigurable"; public static final String USER_SECURITY_GROUP_LIST = "usersecuritygrouplist"; - public static final String USER_SECRET_KEY = "usersecretkey"; public static final String USE_VIRTUAL_NETWORK = "usevirtualnetwork"; public static final String UPDATE_IN_SEQUENCE = "updateinsequence"; public static final String VALUE = "value"; @@ -665,6 +668,7 @@ public class ApiConstants { public static final String ROLE_TYPE = "roletype"; public static final String ROLE_NAME = "rolename"; public static final String PERMISSION = "permission"; + public static final String PERMISSIONS = "permissions"; public static final String RULE = "rule"; public static final String RULES = "rules"; public static final String RULE_ID = "ruleid"; @@ -924,7 +928,7 @@ public class ApiConstants { public static final String NSX_PROVIDER_PORT = "nsxproviderport"; public static final String NSX_CONTROLLER_ID = "nsxcontrollerid"; public static final String S3_ACCESS_KEY = "accesskey"; - public static final String S3_SECRET_KEY = "secretkey"; + public static final String SECRET_KEY = "secretkey"; public static final String S3_END_POINT = "endpoint"; public static final String S3_BUCKET_NAME = "bucket"; public static final String S3_SIGNER = "s3signer"; diff --git a/api/src/main/java/org/apache/cloudstack/api/BaseAsyncCmd.java b/api/src/main/java/org/apache/cloudstack/api/BaseAsyncCmd.java index 6859b0a7f406..3490dec5c260 100644 --- a/api/src/main/java/org/apache/cloudstack/api/BaseAsyncCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/BaseAsyncCmd.java @@ -29,6 +29,8 @@ public abstract class BaseAsyncCmd extends BaseCmd { public static final String migrationSyncObject = "migration"; public static final String snapshotHostSyncObject = "snapshothost"; public static final String gslbSyncObject = "globalserverloadbalancer"; + public static final String apiKeySyncObject = "apikey"; + public static final String user = "user"; private Object job; diff --git a/api/src/main/java/org/apache/cloudstack/api/BaseCmd.java b/api/src/main/java/org/apache/cloudstack/api/BaseCmd.java index 457afdc88478..ead48a47f42f 100644 --- a/api/src/main/java/org/apache/cloudstack/api/BaseCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/BaseCmd.java @@ -35,6 +35,7 @@ import org.apache.cloudstack.acl.ProjectRoleService; import org.apache.cloudstack.acl.RoleService; import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPairService; import org.apache.cloudstack.affinity.AffinityGroupService; import org.apache.cloudstack.alert.AlertService; import org.apache.cloudstack.annotation.AnnotationService; @@ -216,6 +217,8 @@ public static enum CommandType { @Inject public Ipv6Service ipv6Service; @Inject + public ApiKeyPairService apiKeyPairService; + @Inject public VnfTemplateManager vnfTemplateManager; @Inject public BucketApiService _bucketService; diff --git a/api/src/main/java/org/apache/cloudstack/api/ResponseGenerator.java b/api/src/main/java/org/apache/cloudstack/api/ResponseGenerator.java index e2d132c2ae6e..9e64ccf23600 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ResponseGenerator.java +++ b/api/src/main/java/org/apache/cloudstack/api/ResponseGenerator.java @@ -26,6 +26,8 @@ import com.cloud.bgp.ASNumberRange; import org.apache.cloudstack.storage.object.Bucket; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPair; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPairPermission; import org.apache.cloudstack.affinity.AffinityGroup; import org.apache.cloudstack.affinity.AffinityGroupResponse; import org.apache.cloudstack.api.ApiConstants.HostDetails; @@ -44,6 +46,7 @@ import org.apache.cloudstack.api.response.BackupRepositoryResponse; import org.apache.cloudstack.api.response.BackupResponse; import org.apache.cloudstack.api.response.BackupScheduleResponse; +import org.apache.cloudstack.api.response.BaseRolePermissionResponse; import org.apache.cloudstack.api.response.BucketResponse; import org.apache.cloudstack.api.response.CapacityResponse; import org.apache.cloudstack.api.response.ClusterResponse; @@ -79,6 +82,7 @@ import org.apache.cloudstack.api.response.InternalLoadBalancerElementResponse; import org.apache.cloudstack.api.response.IpForwardingRuleResponse; import org.apache.cloudstack.api.response.IsolationMethodResponse; +import org.apache.cloudstack.api.response.ApiKeyPairResponse; import org.apache.cloudstack.api.response.LBHealthCheckResponse; import org.apache.cloudstack.api.response.LBStickinessResponse; import org.apache.cloudstack.api.response.ListResponse; @@ -572,4 +576,8 @@ List createTemplateResponses(ResponseView view, VirtualMachine BackupRepositoryResponse createBackupRepositoryResponse(BackupRepository repository); SharedFSResponse createSharedFSResponse(ResponseView view, SharedFS sharedFS); + + ApiKeyPairResponse createKeyPairResponse(ApiKeyPair keyPair); + + ListResponse createKeypairPermissionsResponse(List permissions); } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/storage/AddImageStoreS3CMD.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/storage/AddImageStoreS3CMD.java index 2fe3c7cd106a..75fcf125eb10 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/storage/AddImageStoreS3CMD.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/storage/AddImageStoreS3CMD.java @@ -27,7 +27,7 @@ import static org.apache.cloudstack.api.ApiConstants.S3_HTTPS_FLAG; import static org.apache.cloudstack.api.ApiConstants.S3_MAX_ERROR_RETRY; import static org.apache.cloudstack.api.ApiConstants.S3_SIGNER; -import static org.apache.cloudstack.api.ApiConstants.S3_SECRET_KEY; +import static org.apache.cloudstack.api.ApiConstants.SECRET_KEY; import static org.apache.cloudstack.api.ApiConstants.S3_SOCKET_TIMEOUT; import static org.apache.cloudstack.api.ApiConstants.S3_USE_TCP_KEEPALIVE; import static org.apache.cloudstack.api.BaseCmd.CommandType.BOOLEAN; @@ -64,7 +64,7 @@ public final class AddImageStoreS3CMD extends BaseCmd implements ClientOptions { @Parameter(name = S3_ACCESS_KEY, type = STRING, required = true, description = "S3 access key") private String accessKey; - @Parameter(name = S3_SECRET_KEY, type = STRING, required = true, description = "S3 secret key") + @Parameter(name = SECRET_KEY, type = STRING, required = true, description = "S3 secret key") private String secretKey; @Parameter(name = S3_END_POINT, type = STRING, required = true, description = "S3 endpoint") @@ -101,7 +101,7 @@ public void execute() throws ResourceUnavailableException, InsufficientCapacityE Map dm = new HashMap(); dm.put(ApiConstants.S3_ACCESS_KEY, getAccessKey()); - dm.put(ApiConstants.S3_SECRET_KEY, getSecretKey()); + dm.put(ApiConstants.SECRET_KEY, getSecretKey()); dm.put(ApiConstants.S3_END_POINT, getEndPoint()); dm.put(ApiConstants.S3_BUCKET_NAME, getBucketName()); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/user/DeleteUserKeysCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/user/DeleteUserKeysCmd.java new file mode 100644 index 000000000000..18ae4fbd34f8 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/user/DeleteUserKeysCmd.java @@ -0,0 +1,79 @@ +// 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 org.apache.cloudstack.api.command.admin.user; + +import com.cloud.event.EventTypes; +import com.cloud.user.Account; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPair; +import org.apache.cloudstack.api.ACL; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiCommandResourceType; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseAsyncCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.response.ApiKeyPairResponse; +import org.apache.cloudstack.api.response.SuccessResponse; + +@APICommand(name = "deleteUserKeys", description = "Deletes a keypair from a user", responseObject = SuccessResponse.class, + since = "4.21.0", requestHasSensitiveInfo = false, responseHasSensitiveInfo = false) +public class DeleteUserKeysCmd extends BaseAsyncCmd { + + @ACL + @Parameter(name = ApiConstants.KEYPAIR_ID, type = CommandType.UUID, entityType = ApiKeyPairResponse.class, required = true, description = "ID of the keypair to be deleted.") + private Long id; + + @Override + public ApiCommandResourceType getApiResourceType() { + return ApiCommandResourceType.User; + } + + @Override + public long getEntityOwnerId() { + ApiKeyPair keyPair = apiKeyPairService.findById(id); + if (keyPair != null) { + return keyPair.getAccountId(); + } + return Account.ACCOUNT_ID_SYSTEM; + } + + public Long getId() { + return id; + } + + @Override + public void execute() { + _accountService.deleteApiKey(this); + + SuccessResponse response = new SuccessResponse(getCommandName()); + this.setResponseObject(response); + } + + @Override + public String getEventType() { + return EventTypes.EVENT_DELETE_SECRET_API_KEY; + } + + @Override + public String getEventDescription() { + return ("Deleting API keypair " + id); + } + + @Override + public Long getSyncObjId() { + return getId(); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/user/GetUserKeysCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/user/GetUserKeysCmd.java index cdd239f72b57..1e88600661fa 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/user/GetUserKeysCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/user/GetUserKeysCmd.java @@ -32,33 +32,35 @@ import java.util.Map; @APICommand(name = "getUserKeys", - description = "This command allows the user to query the seceret and API keys for the account", - responseObject = RegisterResponse.class, - requestHasSensitiveInfo = false, - responseHasSensitiveInfo = true, - authorized = {RoleType.User, RoleType.Admin, RoleType.DomainAdmin, RoleType.ResourceAdmin}, - since = "4.10.0") + description = "Queries the last registered secret and API keys of a user.", + responseObject = RegisterResponse.class, + requestHasSensitiveInfo = false, + responseHasSensitiveInfo = true, + authorized = {RoleType.User, RoleType.Admin, RoleType.DomainAdmin, RoleType.ResourceAdmin}, + since = "4.10.0") -public class GetUserKeysCmd extends BaseCmd{ +public class GetUserKeysCmd extends BaseCmd { - @Parameter(name= ApiConstants.ID, type = CommandType.UUID, entityType = UserResponse.class, required = true, description = "ID of the user whose keys are required") + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = UserResponse.class, required = true, description = "ID of the user whose keys are required") private Long id; - public Long getID(){ + public Long getId(){ return id; - }public long getEntityOwnerId(){ - User user = _entityMgr.findById(User.class, getID()); - if(user != null){ + } + + public long getEntityOwnerId(){ + User user = _entityMgr.findById(User.class, getId()); + if (user != null) { return user.getAccountId(); } - else return Account.ACCOUNT_ID_SYSTEM; + return Account.ACCOUNT_ID_SYSTEM; } - public void execute(){ + public void execute() { Pair> keys = _accountService.getKeys(this); RegisterResponse response = new RegisterResponse(); - if(keys != null){ + if (keys != null){ response.setApiKeyAccess(keys.first()); response.setApiKey(keys.second().get("apikey")); response.setSecretKey(keys.second().get("secretkey")); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/user/ListUserKeyRulesCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/user/ListUserKeyRulesCmd.java new file mode 100644 index 000000000000..814a5aa2683e --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/user/ListUserKeyRulesCmd.java @@ -0,0 +1,70 @@ +// 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 org.apache.cloudstack.api.command.admin.user; + + +import com.cloud.exception.InsufficientCapacityException; +import com.cloud.exception.ResourceUnavailableException; +import com.cloud.user.Account; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPair; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPairPermission; +import org.apache.cloudstack.api.ACL; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseListDomainResourcesCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.ApiKeyPairResponse; +import org.apache.cloudstack.api.response.BaseRolePermissionResponse; +import org.apache.cloudstack.api.response.ListResponse; + +import java.util.List; + +@APICommand(name = "listUserKeyRules", + description = "This command allows the user to query the rules defined for a API access keypair.", + responseObject = BaseRolePermissionResponse.class, + requestHasSensitiveInfo = false, + responseHasSensitiveInfo = false, + since = "4.21.0") + +public class ListUserKeyRulesCmd extends BaseListDomainResourcesCmd { + + @ACL + @Parameter(name=ApiConstants.KEYPAIR_ID, type = CommandType.UUID, entityType = ApiKeyPairResponse.class, description = "ID of the keypair.", required = true) + private Long id; + + public Long getId() { + return id; + } + + public long getEntityOwnerId() { + ApiKeyPair keyPair = apiKeyPairService.findById(getId()); + if (keyPair != null) { + return keyPair.getAccountId(); + } + return Account.ACCOUNT_ID_SYSTEM; + } + + @Override + public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException { + List permissions = _accountService.listKeyRules(this); + ListResponse response = _responseGenerator.createKeypairPermissionsResponse(permissions); + response.setResponseName(getCommandName()); + setResponseObject(response); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/user/ListUserKeysCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/user/ListUserKeysCmd.java new file mode 100644 index 000000000000..a3e8fb2b9e5e --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/user/ListUserKeysCmd.java @@ -0,0 +1,95 @@ +// 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 org.apache.cloudstack.api.command.admin.user; + + +import com.cloud.user.Account; +import com.cloud.user.UserAccount; +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPair; +import org.apache.cloudstack.api.ACL; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseListDomainResourcesCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.api.response.ApiKeyPairResponse; +import org.apache.cloudstack.api.response.UserResponse; + +@APICommand(name = "listUserKeys", + description = "Lists the API key pairs (API and secret keys) of a user.", + responseObject = ApiKeyPairResponse.class, + requestHasSensitiveInfo = false, + responseHasSensitiveInfo = true, + authorized = {RoleType.User, RoleType.Admin, RoleType.DomainAdmin, RoleType.ResourceAdmin}, + since = "4.21.0") + +public class ListUserKeysCmd extends BaseListDomainResourcesCmd { + + @ACL + @Parameter(name = ApiConstants.USER_ID, type = CommandType.UUID, entityType = UserResponse.class, description = "ID of the user that owns the keys.") + private Long userId; + + @ACL + @Parameter(name = ApiConstants.KEYPAIR_ID, type = CommandType.UUID, entityType = ApiKeyPairResponse.class, description = "ID of the keypair.") + private Long keyPairId; + + @ACL + @Parameter(name = ApiConstants.API_KEY_FILTER, type = CommandType.STRING, description = "API Key of the keypair.") + private String apiKeyFilter; + + @Parameter(name = ApiConstants.SHOW_PERMISSIONS, type = CommandType.BOOLEAN, description = "Whether API Key rules should be returned.") + private Boolean showPermissions; + + public Long getUserId() { + return userId; + } + + public Long getKeyId() { + return keyPairId; + } + + public String getApiKeyFilter() { + return apiKeyFilter; + } + public Boolean getShowPermissions() { + return showPermissions; + } + + public long getEntityOwnerId() { + if (getKeyId() != null) { + ApiKeyPair keypair = apiKeyPairService.findById(getKeyId()); + if (keypair != null) { + return keypair.getAccountId(); + } + } else if (getUserId() != null) { + UserAccount userAccount = _accountService.getUserAccountById(getUserId()); + if (userAccount != null) { + return userAccount.getAccountId(); + } + } + return Account.ACCOUNT_ID_SYSTEM; + } + + public void execute() { + ListResponse finalResponse = _accountService.listKeys(this); + finalResponse.setObjectName("userkeys"); + finalResponse.setResponseName(getCommandName()); + setResponseObject(finalResponse); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/user/RegisterCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/user/RegisterCmd.java deleted file mode 100644 index b3e7d2bec821..000000000000 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/user/RegisterCmd.java +++ /dev/null @@ -1,93 +0,0 @@ -// 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 org.apache.cloudstack.api.command.admin.user; - -import org.apache.cloudstack.api.ApiCommandResourceType; - -import org.apache.cloudstack.api.APICommand; -import org.apache.cloudstack.api.ApiConstants; -import org.apache.cloudstack.api.BaseCmd; -import org.apache.cloudstack.api.Parameter; -import org.apache.cloudstack.api.response.RegisterResponse; -import org.apache.cloudstack.api.response.UserResponse; - -import com.cloud.user.Account; -import com.cloud.user.User; - -@APICommand(name = "registerUserKeys", - responseObject = RegisterResponse.class, - description = "This command allows a user to register for the developer API, returning a secret key and an API key. This request is made through the integration API port, so it is a privileged command and must be made on behalf of a user. It is up to the implementer just how the username and password are entered, and then how that translates to an integration API request. Both secret key and API key should be returned to the user", - requestHasSensitiveInfo = false, responseHasSensitiveInfo = true) -public class RegisterCmd extends BaseCmd { - - - ///////////////////////////////////////////////////// - //////////////// API parameters ///////////////////// - ///////////////////////////////////////////////////// - - @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = UserResponse.class, required = true, description = "User id") - private Long id; - - ///////////////////////////////////////////////////// - /////////////////// Accessors /////////////////////// - ///////////////////////////////////////////////////// - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - ///////////////////////////////////////////////////// - /////////////// API Implementation/////////////////// - ///////////////////////////////////////////////////// - - @Override - public long getEntityOwnerId() { - User user = _entityMgr.findById(User.class, getId()); - if (user != null) { - return user.getAccountId(); - } - - return Account.ACCOUNT_ID_SYSTEM; // no account info given, parent this command to SYSTEM so ERROR events are tracked - } - - @Override - public Long getApiResourceId() { - return id; - } - - @Override - public ApiCommandResourceType getApiResourceType() { - return ApiCommandResourceType.User; - } - - @Override - public void execute() { - String[] keys = _accountService.createApiKeyAndSecretKey(this); - RegisterResponse response = new RegisterResponse(); - if (keys != null) { - response.setApiKey(keys[0]); - response.setSecretKey(keys[1]); - } - response.setObjectName("userkeys"); - response.setResponseName(getCommandName()); - this.setResponseObject(response); - } -} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/user/RegisterUserKeysCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/user/RegisterUserKeysCmd.java new file mode 100644 index 000000000000..6d3423570851 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/user/RegisterUserKeysCmd.java @@ -0,0 +1,205 @@ +// 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 org.apache.cloudstack.api.command.admin.user; + +import com.cloud.event.EventTypes; +import com.cloud.user.Account; +import com.cloud.user.User; +import org.apache.cloudstack.acl.Rule; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPair; +import org.apache.cloudstack.api.ApiCommandResourceType; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseAsyncCmd; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.commons.lang3.StringUtils; + +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.response.ApiKeyPairResponse; +import org.apache.cloudstack.api.response.UserResponse; + +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@APICommand(name = "registerUserKeys", + responseObject = ApiKeyPairResponse.class, + description = "This command allows a user to register for the developer API, returning a secret key and an API key. This request is made through the integration API port, so it is a privileged command and must be made on behalf of a user. It is up to the implementer just how the username and password are entered, and then how that translates to an integration API request. Both secret key and API key should be returned to the user", + requestHasSensitiveInfo = false, responseHasSensitiveInfo = true) +public class RegisterUserKeysCmd extends BaseAsyncCmd { + + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = UserResponse.class, required = true, description = "User ID.") + private Long id; + + @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, description = "API keypair name.") + private String name; + + @Parameter(name = ApiConstants.DESCRIPTION, type = CommandType.STRING, description = "API keypair description.") + private String description; + + @Parameter(name = ApiConstants.START_DATE, type = CommandType.DATE, description = "Start date of the API keypair. " + + ApiConstants.PARAMETER_DESCRIPTION_START_DATE_POSSIBLE_FORMATS) + private Date startDate; + + @Parameter(name = ApiConstants.END_DATE, type = CommandType.DATE, description = "Expiration date of the API keypair. " + + ApiConstants.PARAMETER_DESCRIPTION_END_DATE_POSSIBLE_FORMATS) + private Date endDate; + + @Parameter(name = ApiConstants.RULES, type = CommandType.MAP, description = "Rules param list, lower indexed rules take precedence over higher. If no rules are informed, " + + "defaults to allowing all account permissions. Example input: rules[0].rule=* rules[0].permission=allow") + private Map rules; + + public void setUserId(Long userId) { + this.id = userId; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public Date getStartDate() { + return startDate; + } + + public void setStartDate(Date startDate) { + this.startDate = startDate; + } + + public Date getEndDate() { + return endDate; + } + + public void setEndDate(Date endDate) { + this.endDate = endDate; + } + + public void setRules(Map rules) { + this.rules = rules; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + public List> getRules() { + List> rulesDetails = new ArrayList<>(); + + if (rules == null) { + return rulesDetails; + } + + for (Object ruleObject : rules.values()) { + HashMap detail = (HashMap) ruleObject; + Map ruleDetails = new HashMap<>(); + String rule = detail.get(ApiConstants.RULE); + + ruleDetails.put(ApiConstants.RULE, new Rule(rule)); + + String permission = detail.get(ApiConstants.PERMISSION); + if (StringUtils.isEmpty(permission)) { + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, String.format("Rule %s has no permission associated with it," + + " please specify if it is either allow or deny.", rule)); + } + ruleDetails.put(ApiConstants.PERMISSION, roleService.getRolePermission(permission)); + + String description = detail.get(ApiConstants.DESCRIPTION); + if (StringUtils.isNotEmpty(description)) { + ruleDetails.put(ApiConstants.DESCRIPTION, description); + } + + rulesDetails.add(ruleDetails); + } + return rulesDetails; + } + + @Override + public long getEntityOwnerId() { + User user = _entityMgr.findById(User.class, getUserId()); + List accessableUsers = _queryService.searchForAccessableUsers(); + if (user != null && accessableUsers.stream().anyMatch(u -> u == user.getId())) { + return user.getAccountId(); + } + return Account.ACCOUNT_ID_SYSTEM; + } + + public Long getUserId() { + return id; + } + + @Override + public Long getApiResourceId() { + User user = _entityMgr.findById(User.class, getUserId()); + if (user != null) { + return user.getId(); + } + return null; + } + + @Override + public ApiCommandResourceType getApiResourceType() { + return ApiCommandResourceType.User; + } + + @Override + public void execute() { + _accountService.validateCallingUserHasAccessToDesiredUser(id); + + ApiKeyPair apiKeyPair = _accountService.createApiKeyAndSecretKey(this); + ApiKeyPairResponse response = new ApiKeyPairResponse(); + if (apiKeyPair != null) { + response = _responseGenerator.createKeyPairResponse(apiKeyPair); + } + response.setObjectName("userkeys"); + response.setResponseName(getCommandName()); + this.setResponseObject(response); + } + + @Override + public String getEventType() { + return EventTypes.EVENT_REGISTER_FOR_SECRET_API_KEY; + } + + @Override + public String getEventDescription() { + return String.format("Registering API keypair for user [%s].", getUserId()); + } + + @Override + public String getSyncObjType() { + return BaseAsyncCmd.user; + } + + @Override + public Long getSyncObjId() { + return getUserId(); + } +} 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 3d7f51ae2204..d513010304a8 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 @@ -44,7 +44,7 @@ public class UpdateUserCmd extends BaseCmd { ///////////////////////////////////////////////////// @Parameter(name = ApiConstants.USER_API_KEY, type = CommandType.STRING, description = "The API key for the user. Must be specified with userSecretKey") - private String apiKey; + private String userApiKey; @Parameter(name = ApiConstants.EMAIL, type = CommandType.STRING, description = "email") private String email; @@ -67,8 +67,8 @@ public class UpdateUserCmd extends BaseCmd { @Parameter(name = ApiConstants.CURRENT_PASSWORD, type = CommandType.STRING, description = "Current password that was being used by the user. You must inform the current password when updating the password.", acceptedOnAdminPort = false) private String currentPassword; - @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.USER_SECRET_KEY, type = CommandType.STRING, description = "The secret key for the user. Must be specified with userApiKey.") + private String userSecretKey; @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; @@ -93,7 +93,7 @@ public class UpdateUserCmd extends BaseCmd { ///////////////////////////////////////////////////// public String getApiKey() { - return apiKey; + return userApiKey; } public String getEmail() { @@ -121,7 +121,7 @@ public String getCurrentPassword() { } public String getSecretKey() { - return secretKey; + return userSecretKey; } public String getApiKeyAccess() { diff --git a/api/src/main/java/org/apache/cloudstack/api/response/ApiKeyPairResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/ApiKeyPairResponse.java new file mode 100644 index 000000000000..7f8ba60a95e6 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/response/ApiKeyPairResponse.java @@ -0,0 +1,285 @@ +// 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 org.apache.cloudstack.api.response; + +import com.cloud.user.ApiKeyPairState; +import com.google.gson.annotations.SerializedName; + +import java.util.Date; +import java.util.List; + +import org.apache.cloudstack.acl.apikeypair.ApiKeyPair; +import org.apache.cloudstack.api.ApiConstants; + +import com.cloud.serializer.Param; +import org.apache.cloudstack.api.BaseResponseWithAnnotations; +import org.apache.cloudstack.api.EntityReference; + +@EntityReference(value = ApiKeyPair.class) +public class ApiKeyPairResponse extends BaseResponseWithAnnotations { + @SerializedName(ApiConstants.NAME) + @Param(description = "Name of the Keypair") + private String name; + + @SerializedName(ApiConstants.API_KEY) + @Param(description = "The API key of the registered user.", isSensitive = true) + private String userApiKey; + + @SerializedName(ApiConstants.SECRET_KEY) + @Param(description = "The secret key of the registered user.", isSensitive = true) + private String userSecretKey; + + @SerializedName(ApiConstants.USER_ID) + @Param(description = "ID of the user that owns the keypair.") + private String userId; + + @SerializedName(ApiConstants.USERNAME) + @Param(description = "Username of the keypair's owner.") + private String username; + + @SerializedName(ApiConstants.ID) + @Param(description = "ID of the API keypair.", isSensitive = true) + private String id; + + @SerializedName(ApiConstants.DESCRIPTION) + @Param(description = "Keypair description.") + private String description; + + @SerializedName(ApiConstants.START_DATE) + @Param(description = "Keypair start date.") + private Date startDate; + + @SerializedName(ApiConstants.END_DATE) + @Param(description = "Keypair expiration date.") + private Date endDate; + + @SerializedName(ApiConstants.CREATED) + @Param(description = "Keypair creation timestamp.") + private Date created; + + @SerializedName(ApiConstants.ACCOUNT_TYPE) + @Param(description = "Account type.") + private String accountType; + + @SerializedName(ApiConstants.ACCOUNT_ID) + @Param(description = "Account ID.") + private String accountId; + + @SerializedName(ApiConstants.ACCOUNT_NAME) + @Param(description = "Account name.") + private String accountName; + + @SerializedName(ApiConstants.ROLE_ID) + @Param(description = "ID of the role.") + private String roleId; + + @SerializedName(ApiConstants.ROLE_TYPE) + @Param(description = "Type of the role (Admin, ResourceAdmin, DomainAdmin, User).") + private String roleType; + + @SerializedName(ApiConstants.ROLE_NAME) + @Param(description = "Name of the role.") + private String roleName; + + @SerializedName(ApiConstants.PERMISSIONS) + @Param(description = "Permissions of the keypair.") + private List permissions; + + @SerializedName(ApiConstants.DOMAIN_ID) + @Param(description = "ID of the domain which the account belongs to.") + private String domainId; + + @SerializedName(ApiConstants.DOMAIN) + @Param(description = "Name of the domain which the account belongs to.") + private String domainName; + + @SerializedName(ApiConstants.DOMAIN_PATH) + @Param(description = "Path of the domain which the account belongs to.") + private String domainPath; + + @SerializedName(ApiConstants.STATE) + @Param(description = "State of the keypair.") + private ApiKeyPairState state; + + public String getApiKey() { + return userApiKey; + } + + public void setApiKey(String apiKey) { + this.userApiKey = apiKey; + } + + public String getSecretKey() { + return userSecretKey; + } + + public void setSecretKey(String secretKey) { + this.userSecretKey = secretKey; + } + + public String getId() { + return this.id; + } + + public void setId(String id) { + this.id = id; + } + + public String getUserId() { + return userId; + } + + public void setUserId(String userId) { + this.userId = userId; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public Date getStartDate() { + return startDate; + } + + public void setStartDate(Date startDate) { + this.startDate = startDate; + } + + public Date getEndDate() { + return endDate; + } + + public void setEndDate(Date endDate) { + this.endDate = endDate; + } + + public Date getCreated() { + return created; + } + + public void setCreated(Date created) { + this.created = created; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getAccountType() { + return accountType; + } + + public void setAccountType(String accountType) { + this.accountType = accountType; + } + + public String getRoleId() { + return roleId; + } + + public void setRoleId(String roleId) { + this.roleId = roleId; + } + + public String getAccountId() { + return accountId; + } + + public void setAccountId(String accountId) { + this.accountId = accountId; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getRoleType() { + return roleType; + } + + public void setRoleType(String roleType) { + this.roleType = roleType; + } + + public String getRoleName() { + return roleName; + } + + public void setRoleName(String roleName) { + this.roleName = roleName; + } + + public String getDomainId() { + return domainId; + } + + public void setDomainId(String domainId) { + this.domainId = domainId; + } + + public String getDomainName() { + return domainName; + } + + public void setDomainName(String domainName) { + this.domainName = domainName; + } + + public String getDomainPath() { + return domainPath; + } + + public void setDomainPath(String domainPath) { + this.domainPath = domainPath; + } + + public ApiKeyPairState getState() { + return state; + } + + public void setState(ApiKeyPairState state) { + this.state = state; + } + + public String getAccountName() { + return accountName; + } + + public void setAccountName(String accountName) { + this.accountName = accountName; + } + + public List getPermissions() { + return permissions; + } + + public void setPermissions(List permissions) { + this.permissions = permissions; + } +} 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 5e4e6e1f3c8b..8bd1637a5d4f 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 @@ -95,15 +95,6 @@ public class UserResponse extends BaseResponse implements SetResourceIconRespons @Param(description = "the timezone user was created in") private String timezone; - @SerializedName("apikey") - @Param(description = "the api key of the user", isSensitive = true) - private String apiKey; - - @Deprecated - @SerializedName("secretkey") - @Param(description = "the secret key of the user", isSensitive = true) - private String secretKey; - @SerializedName("accountid") @Param(description = "the account ID of the user") private String accountId; @@ -132,6 +123,16 @@ public class UserResponse extends BaseResponse implements SetResourceIconRespons @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; + @SerializedName(ApiConstants.API_KEY) + @Param(description = "the api key of the user", isSensitive = true) + private String apiKey; + + @Deprecated + @SerializedName(ApiConstants.SECRET_KEY) + @Param(description = "the secret key of the user", isSensitive = true) + private String secretKey; + + @Override public String getObjectId() { return this.getId(); diff --git a/api/src/main/java/org/apache/cloudstack/query/QueryService.java b/api/src/main/java/org/apache/cloudstack/query/QueryService.java index 4278c9217b5a..c915bae04cce 100644 --- a/api/src/main/java/org/apache/cloudstack/query/QueryService.java +++ b/api/src/main/java/org/apache/cloudstack/query/QueryService.java @@ -141,6 +141,8 @@ public interface QueryService { ListResponse searchForUsers(Long domainId, boolean recursive) throws PermissionDeniedException; + List searchForAccessableUsers(); + ListResponse searchForEvents(ListEventsCmd cmd); ListResponse listTags(ListTagsCmd cmd); diff --git a/api/src/test/java/org/apache/cloudstack/acl/RuleTest.java b/api/src/test/java/org/apache/cloudstack/acl/RuleTest.java index 79e6127d29ad..b99ba48c66dc 100644 --- a/api/src/test/java/org/apache/cloudstack/acl/RuleTest.java +++ b/api/src/test/java/org/apache/cloudstack/acl/RuleTest.java @@ -17,13 +17,46 @@ package org.apache.cloudstack.acl; import com.cloud.exception.InvalidParameterValueException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Pattern; +import org.apache.cloudstack.api.APICommand; import org.junit.Assert; +import org.junit.BeforeClass; import org.junit.Test; import java.util.Arrays; +import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; +import org.springframework.core.type.filter.AnnotationTypeFilter; public class RuleTest { + private static List apiNames; + private static List apiRules; + private static ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false); + + @BeforeClass + public static void setup() { + provider.addIncludeFilter(new AnnotationTypeFilter(APICommand.class)); + Set beanDefinitions = provider.findCandidateComponents("org.apache.cloudstack.api"); + + apiNames = new ArrayList<>(); + apiRules = new ArrayList<>(); + for(BeanDefinition bd : beanDefinitions) { + if (bd instanceof AnnotatedBeanDefinition) { + Map annotationAttributeMap = ((AnnotatedBeanDefinition) bd).getMetadata() + .getAnnotationAttributes(APICommand.class.getName()); + String apiName = annotationAttributeMap.get("name").toString(); + apiNames.add(apiName); + apiRules.add(new Rule(apiName)); + } + } + } + @Test public void testToString() throws Exception { Rule rule = new Rule("someString"); @@ -31,21 +64,89 @@ public void testToString() throws Exception { } @Test - public void testMatchesEmpty() throws Exception { - Rule rule = new Rule("someString"); - Assert.assertFalse(rule.matches("")); + public void ruleMatchesTestNoMatchesOnEmptyString() throws Exception { + String testCmd = ""; + List matches = new ArrayList<>(); + for (Rule rule : apiRules) { + if (rule.matches(testCmd)) { + matches.add(rule.getRuleString()); + } + } + + Assert.assertEquals(matches.size(), 0); } @Test - public void testMatchesNull() throws Exception { - Rule rule = new Rule("someString"); - Assert.assertFalse(rule.matches(null)); + public void ruleMatchesTestNoMatchesOnNull() throws Exception { + List matches = new ArrayList<>(); + for (Rule rule : apiRules) { + if (rule.matches(null)) { + matches.add(rule.getRuleString()); + } + } + + Assert.assertTrue(matches.isEmpty()); } @Test - public void testMatchesSpace() throws Exception { - Rule rule = new Rule("someString"); - Assert.assertFalse(rule.matches(" ")); + public void ruleMatchesTestNoMatchesOnSpaceCharacter() throws Exception { + String testCmd = " "; + List matches = new ArrayList<>(); + for (Rule rule : apiRules) { + if (rule.matches(testCmd)) { + matches.add(rule.getRuleString()); + } + } + + Assert.assertTrue(matches.isEmpty()); + } + + @Test + public void ruleMatchesTestWildCardOnEndWorksAsNormalRegex() { + setup(); + Pattern regexPattern = Pattern.compile("list.*"); + Rule acsRegexRule = new Rule("list*"); + + List nonMatches = new ArrayList<>(); + for (String apiName : apiNames) { + if (acsRegexRule.matches(apiName) != regexPattern.matcher(apiName).matches()) { + nonMatches.add(apiName); + } + } + + Assert.assertTrue(nonMatches.isEmpty()); + } + + @Test + public void ruleMatchesTestWildCardOnMiddleWorksAsNormalRegex() { + setup(); + Pattern regexPattern = Pattern.compile("list.*s"); + Rule acsRegexRule = new Rule("list*s"); + + List nonMatches = new ArrayList<>(); + for (String apiName : apiNames) { + if (acsRegexRule.matches(apiName) != regexPattern.matcher(apiName).matches()) { + nonMatches.add(apiName); + } + } + + Assert.assertTrue(nonMatches.isEmpty()); + } + + @Test + public void ruleMatchesTestWildCardOnStartWorksAsNormalRegex() { + setup(); + Pattern regexPattern = Pattern.compile(".*User"); + Rule acsRegexRule = new Rule("*User"); + + List nonMatches = new ArrayList<>(); + for (String apiName : apiNames) { + if (acsRegexRule.matches(apiName) != regexPattern.matcher(apiName).matches()) { + nonMatches.add(apiName); + } + } + + Assert.assertTrue(nonMatches.isEmpty()); } @Test @@ -73,7 +174,25 @@ public void testMatchesWildcardMiddle() throws Exception { } @Test - public void testValidateRuleWithValidData() throws Exception { + public void ruleMatchesTestWildcardOnRuleAndCommand() throws Exception { + Rule rule = new Rule("*"); + Assert.assertTrue(rule.matches("list*")); + } + + @Test + public void ruleMatchesTestWildcardOnRuleAndCommandNotAllowed() throws Exception { + Rule rule = new Rule("list*"); + Assert.assertFalse(rule.matches("*")); + } + + @Test + public void ruleMatchesTestWithMultipleStars() throws Exception { + Rule rule = new Rule("list***"); + Assert.assertFalse(rule.matches("api")); + } + + @Test + public void testRuleToStringWithValidStrings() throws Exception { for (String rule : Arrays.asList("a", "1", "someApi", "someApi321", "123SomeApi", "prefix*", "*middle*", "*Suffix", "*", "**", "f***", "m0nk3yMa**g1c*")) { @@ -82,7 +201,7 @@ public void testValidateRuleWithValidData() throws Exception { } @Test - public void testValidateRuleWithInvalidData() throws Exception { + public void testRuleToStringWithInvalidStrings() throws Exception { for (String rule : Arrays.asList(null, "", " ", " ", "\n", "\t", "\r", "\"", "\'", "^someApi$", "^someApi", "some$", "some-Api;", "some,Api", "^", "$", "^$", ".*", "\\w+", "r**l3rd0@Kr3", "j@s1n|+|0È·", diff --git a/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade410to420.java b/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade410to420.java index b78aed3119a4..abf70154c658 100644 --- a/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade410to420.java +++ b/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade410to420.java @@ -1947,7 +1947,7 @@ private void migrateS3ToImageStore(Connection conn) { Map detailMap = new HashMap(); detailMap.put(ApiConstants.S3_ACCESS_KEY, s3_accesskey); - detailMap.put(ApiConstants.S3_SECRET_KEY, s3_secretkey); + detailMap.put(ApiConstants.SECRET_KEY, s3_secretkey); detailMap.put(ApiConstants.S3_BUCKET_NAME, s3_bucket); detailMap.put(ApiConstants.S3_END_POINT, s3_endpoint); detailMap.put(ApiConstants.S3_HTTPS_FLAG, String.valueOf(s3_https)); diff --git a/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42010to42100.java b/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42010to42100.java index d6dc85dbb9aa..b80763177c5b 100644 --- a/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42010to42100.java +++ b/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42010to42100.java @@ -19,7 +19,12 @@ import com.cloud.upgrade.SystemVmTemplateRegistration; import com.cloud.utils.db.TransactionLegacy; import com.cloud.utils.exception.CloudRuntimeException; - +import java.sql.Date; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.time.LocalDate; +import java.util.UUID; import java.io.InputStream; import java.sql.Connection; import java.sql.PreparedStatement; @@ -57,9 +62,52 @@ public InputStream[] getPrepareScripts() { return new InputStream[] {script}; } + private void performKeyPairMigration(Connection conn) throws SQLException { + try { + logger.debug("Performing keypair migration from user table to api_keypair table."); + PreparedStatement pstmt = conn.prepareStatement("SELECT u.id, u.api_key, u.secret_key, a.domain_id, u.id FROM `cloud`.`user` AS u JOIN `cloud`.`account` AS a " + + "ON u.account_id = a.id WHERE u.api_key IS NOT NULL AND u.secret_key IS NOT NULL"); + ResultSet resultSet = pstmt.executeQuery(); + + while (resultSet.next()) { + long id = resultSet.getLong(1); + String apiKey = resultSet.getString(2); + String secretKey = resultSet.getString(3); + Long domainId = resultSet.getLong(4); + Long accountId = resultSet.getLong(5); + Date timestamp = Date.valueOf(LocalDate.now()); + + PreparedStatement preparedStatement = conn.prepareStatement("INSERT IGNORE INTO `cloud`.`api_keypair` (uuid, user_id, domain_id, account_id, api_key, secret_key, created, name) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"); + String uuid = UUID.randomUUID().toString(); + preparedStatement.setString(1, uuid); + preparedStatement.setLong(2, id); + preparedStatement.setLong(3, domainId); + preparedStatement.setLong(4, accountId); + + preparedStatement.setString(5, apiKey); + preparedStatement.setString(6, secretKey); + preparedStatement.setDate(7, timestamp); + preparedStatement.setString(8, uuid); + + preparedStatement.executeUpdate(); + } + pstmt = conn.prepareStatement("ALTER TABLE `cloud`.`user` DROP COLUMN IF EXISTS api_key, DROP COLUMN IF EXISTS secret_key;"); + pstmt.executeUpdate(); + logger.info("Successfully performed keypair migration."); + } catch (SQLException ex) { + logger.info("Unexpected exception in user keypair migration", ex); + throw ex; + } + } + @Override public void performDataMigration(Connection conn) { migrateConfigurationScopeToBitmask(conn); + try { + performKeyPairMigration(conn); + } catch (SQLException e) { + throw new RuntimeException(e); + } } @Override diff --git a/engine/schema/src/main/java/com/cloud/user/UserAccountVO.java b/engine/schema/src/main/java/com/cloud/user/UserAccountVO.java index e4fcbad6b02f..8856522beedf 100644 --- a/engine/schema/src/main/java/com/cloud/user/UserAccountVO.java +++ b/engine/schema/src/main/java/com/cloud/user/UserAccountVO.java @@ -36,7 +36,6 @@ import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; import org.apache.commons.lang3.StringUtils; -import com.cloud.utils.db.Encrypt; import com.cloud.utils.db.GenericDao; @Entity @@ -69,13 +68,6 @@ public class UserAccountVO implements UserAccount, InternalIdentity { @Column(name = "state") private String state; - @Column(name = "api_key") - private String apiKey = null; - - @Encrypt - @Column(name = "secret_key") - private String secretKey = null; - @Column(name = GenericDao.CREATED_COLUMN) private Date created; @@ -203,24 +195,6 @@ public void setState(String state) { this.state = state; } - @Override - public String getApiKey() { - return apiKey; - } - - public void setApiKey(String apiKey) { - this.apiKey = apiKey; - } - - @Override - public String getSecretKey() { - return secretKey; - } - - public void setSecretKey(String secretKey) { - this.secretKey = secretKey; - } - @Override public Date getCreated() { return created; 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 6e355e102e6c..3cc2523b4a90 100644 --- a/engine/schema/src/main/java/com/cloud/user/UserVO.java +++ b/engine/schema/src/main/java/com/cloud/user/UserVO.java @@ -33,7 +33,6 @@ import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; import com.cloud.user.Account.State; -import com.cloud.utils.db.Encrypt; import com.cloud.utils.db.GenericDao; import org.apache.commons.lang3.StringUtils; @@ -71,13 +70,6 @@ public class UserVO implements User, Identity, InternalIdentity { @Enumerated(value = EnumType.STRING) private State state; - @Column(name = "api_key") - private String apiKey = null; - - @Encrypt - @Column(name = "secret_key") - private String secretKey = null; - @Column(name = GenericDao.CREATED_COLUMN) private Date created; @@ -150,8 +142,6 @@ public UserVO(UserVO user) { this.setTimezone(user.getTimezone()); this.setUuid(user.getUuid()); this.setSource(user.getSource()); - this.setApiKey(user.getApiKey()); - this.setSecretKey(user.getSecretKey()); this.setExternalEntity(user.getExternalEntity()); this.setRegistered(user.isRegistered()); this.setRegistrationToken(user.getRegistrationToken()); @@ -243,26 +233,6 @@ public void setState(State state) { this.state = state; } - @Override - public String getApiKey() { - return apiKey; - } - - @Override - public void setApiKey(String apiKey) { - this.apiKey = apiKey; - } - - @Override - public String getSecretKey() { - return secretKey; - } - - @Override - public void setSecretKey(String secretKey) { - this.secretKey = secretKey; - } - @Override public String getTimezone() { if (StringUtils.isEmpty(timezone)) { diff --git a/engine/schema/src/main/java/com/cloud/user/dao/AccountDao.java b/engine/schema/src/main/java/com/cloud/user/dao/AccountDao.java index dae5f3a34677..910efed34ef2 100644 --- a/engine/schema/src/main/java/com/cloud/user/dao/AccountDao.java +++ b/engine/schema/src/main/java/com/cloud/user/dao/AccountDao.java @@ -23,11 +23,13 @@ import com.cloud.user.AccountVO; import com.cloud.user.User; import com.cloud.utils.Pair; +import com.cloud.utils.Ternary; import com.cloud.utils.db.Filter; import com.cloud.utils.db.GenericDao; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPair; public interface AccountDao extends GenericDao { - Pair findUserAccountByApiKey(String apiKey); + Ternary findUserAccountByApiKey(String apiKey); List findAccountsLike(String accountName); 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 f5f95d5da1ff..c47290b1731e 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 @@ -30,6 +30,7 @@ import com.cloud.user.User; import com.cloud.user.UserVO; import com.cloud.utils.Pair; +import com.cloud.utils.Ternary; import com.cloud.utils.crypt.DBEncryptionUtil; import com.cloud.utils.db.Filter; import com.cloud.utils.db.GenericDaoBase; @@ -38,13 +39,16 @@ import com.cloud.utils.db.SearchCriteria; import com.cloud.utils.db.SearchCriteria.Func; import com.cloud.utils.db.SearchCriteria.Op; +import org.apache.cloudstack.acl.ApiKeyPairVO; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPair; import com.cloud.utils.db.TransactionLegacy; @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, 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"; + private static final String FIND_USER_ACCOUNT_BY_API_KEY = "SELECT u.id, u.username, u.account_id, 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, ak.id, ak.start_date, ak.end_date, ak.secret_key, " + + "ak.removed FROM `cloud`.`user` u INNER JOIN `cloud`.`account` a ON u.account_id = a.id INNER JOIN `cloud`.`api_keypair` ak " + + "ON ak.user_id = u.id WHERE ak.api_key = ? AND u.removed IS NULL"; protected final SearchBuilder AllFieldsSearch; protected final SearchBuilder AccountTypeSearch; @@ -133,48 +137,53 @@ public List findCleanupsForDisabledAccounts() { } @Override - public Pair findUserAccountByApiKey(String apiKey) { + public Ternary findUserAccountByApiKey(String apiKey) { TransactionLegacy txn = TransactionLegacy.currentTxn(); - PreparedStatement pstmt = null; - Pair userAcctPair = null; + PreparedStatement pstmt; + Ternary userAcctTernary = null; try { - String sql = FIND_USER_ACCOUNT_BY_API_KEY; - pstmt = txn.prepareAutoCloseStatement(sql); + pstmt = txn.prepareAutoCloseStatement(FIND_USER_ACCOUNT_BY_API_KEY); pstmt.setString(1, apiKey); ResultSet rs = pstmt.executeQuery(); // TODO: make sure we don't have more than 1 result? ApiKey had better be unique if (rs.next()) { - User u = new UserVO(rs.getLong(1)); - u.setUsername(rs.getString(2)); - u.setAccountId(rs.getLong(3)); - u.setSecretKey(DBEncryptionUtil.decrypt(rs.getString(4))); - u.setState(State.getValueOf(rs.getString(5))); - boolean apiKeyAccess = rs.getBoolean(6); + User user = new UserVO(rs.getLong(1)); + user.setUsername(rs.getString(2)); + user.setAccountId(rs.getLong(3)); + user.setState(State.getValueOf(rs.getString(4))); + boolean apiKeyAccess = rs.getBoolean(5); if (rs.wasNull()) { - u.setApiKeyAccess(null); + user.setApiKeyAccess(null); } else { - u.setApiKeyAccess(apiKeyAccess); + user.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); + AccountVO account = new AccountVO(rs.getLong(6)); + account.setAccountName(rs.getString(7)); + account.setType(Account.Type.getFromValue(rs.getInt(8))); + account.setRoleId(rs.getLong(9)); + account.setDomainId(rs.getLong(10)); + account.setState(State.getValueOf(rs.getString(11))); + apiKeyAccess = rs.getBoolean(12); if (rs.wasNull()) { - a.setApiKeyAccess(null); + account.setApiKeyAccess(null); } else { - a.setApiKeyAccess(apiKeyAccess); + account.setApiKeyAccess(apiKeyAccess); } - userAcctPair = new Pair(u, a); + ApiKeyPairVO keyPair = new ApiKeyPairVO(rs.getLong(13)); + keyPair.setStartDate(rs.getTimestamp(14)); + keyPair.setEndDate(rs.getTimestamp(15)); + keyPair.setSecretKey(DBEncryptionUtil.decrypt(rs.getString(16))); + keyPair.setRemoved(rs.getTimestamp(17)); + keyPair.setApiKey(apiKey); + + userAcctTernary = new Ternary<>(user, account, keyPair); } } catch (Exception e) { - logger.warn("Exception finding user/acct by api key: " + apiKey, e); + logger.warn("Exception finding user/acct by api key: {}", apiKey, e); } - return userAcctPair; + return userAcctTernary; } @Override diff --git a/engine/schema/src/main/java/com/cloud/user/dao/UserAccountDao.java b/engine/schema/src/main/java/com/cloud/user/dao/UserAccountDao.java index de3b769571e9..e377bbab94ed 100644 --- a/engine/schema/src/main/java/com/cloud/user/dao/UserAccountDao.java +++ b/engine/schema/src/main/java/com/cloud/user/dao/UserAccountDao.java @@ -30,6 +30,4 @@ public interface UserAccountDao extends GenericDao { List getUserAccountByEmail(String email, Long domainId); boolean validateUsernameInDomain(String username, Long domainId); - - UserAccount getUserByApiKey(String apiKey); } diff --git a/engine/schema/src/main/java/com/cloud/user/dao/UserAccountDaoImpl.java b/engine/schema/src/main/java/com/cloud/user/dao/UserAccountDaoImpl.java index c9de9a367eed..28392abbff5c 100644 --- a/engine/schema/src/main/java/com/cloud/user/dao/UserAccountDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/user/dao/UserAccountDaoImpl.java @@ -19,7 +19,6 @@ import com.cloud.user.UserAccount; import com.cloud.user.UserAccountVO; import com.cloud.utils.db.GenericDaoBase; -import com.cloud.utils.db.SearchBuilder; import com.cloud.utils.db.SearchCriteria; import org.springframework.stereotype.Component; @@ -28,14 +27,6 @@ @Component public class UserAccountDaoImpl extends GenericDaoBase implements UserAccountDao { - protected final SearchBuilder userAccountSearch; - - public UserAccountDaoImpl() { - userAccountSearch = createSearchBuilder(); - userAccountSearch.and("apiKey", userAccountSearch.entity().getApiKey(), SearchCriteria.Op.EQ); - userAccountSearch.done(); - } - @Override public List getAllUsersByNameAndEntity(String username, String entity) { if (username == null) { @@ -79,12 +70,4 @@ public boolean validateUsernameInDomain(String username, Long domainId) { } return false; } - - @Override - public UserAccount getUserByApiKey(String apiKey) { - SearchCriteria sc = userAccountSearch.create(); - sc.setParameters("apiKey", apiKey); - return findOneBy(sc); - } - } diff --git a/engine/schema/src/main/java/com/cloud/user/dao/UserDao.java b/engine/schema/src/main/java/com/cloud/user/dao/UserDao.java index 14b074251508..2e160efb9506 100644 --- a/engine/schema/src/main/java/com/cloud/user/dao/UserDao.java +++ b/engine/schema/src/main/java/com/cloud/user/dao/UserDao.java @@ -37,13 +37,6 @@ public interface UserDao extends GenericDao { List listByAccount(long accountId); - /** - * Finds a user based on the secret key provided. - * @param secretKey - * @return - */ - UserVO findUserBySecretKey(String secretKey); - /** * Finds a user based on the registration token provided. * @param registrationToken diff --git a/engine/schema/src/main/java/com/cloud/user/dao/UserDaoImpl.java b/engine/schema/src/main/java/com/cloud/user/dao/UserDaoImpl.java index 8baf732c2406..de60e48dff8f 100644 --- a/engine/schema/src/main/java/com/cloud/user/dao/UserDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/user/dao/UserDaoImpl.java @@ -65,10 +65,6 @@ protected UserDaoImpl() { UserIdSearch.and("id", UserIdSearch.entity().getId(), SearchCriteria.Op.EQ); UserIdSearch.done(); - SecretKeySearch = createSearchBuilder(); - SecretKeySearch.and("secretKey", SecretKeySearch.entity().getSecretKey(), SearchCriteria.Op.EQ); - SecretKeySearch.done(); - RegistrationTokenSearch = createSearchBuilder(); RegistrationTokenSearch.and("registrationToken", RegistrationTokenSearch.entity().getRegistrationToken(), SearchCriteria.Op.EQ); RegistrationTokenSearch.done(); @@ -121,13 +117,6 @@ public List findUsersLike(String username) { return listBy(sc); } - @Override - public UserVO findUserBySecretKey(String secretKey) { - SearchCriteria sc = SecretKeySearch.create(); - sc.setParameters("secretKey", secretKey); - return findOneBy(sc); - } - @Override public UserVO findUserByRegistrationToken(String registrationToken) { SearchCriteria sc = RegistrationTokenSearch.create(); diff --git a/engine/schema/src/main/java/org/apache/cloudstack/acl/ApiKeyPairPermissionVO.java b/engine/schema/src/main/java/org/apache/cloudstack/acl/ApiKeyPairPermissionVO.java new file mode 100644 index 000000000000..1506f2b47ff4 --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/acl/ApiKeyPairPermissionVO.java @@ -0,0 +1,57 @@ +// 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 org.apache.cloudstack.acl; + +import org.apache.cloudstack.acl.apikeypair.ApiKeyPairPermission; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; + +@Entity +@Table(name = "keypair_permissions") +public class ApiKeyPairPermissionVO extends RolePermissionBaseVO implements ApiKeyPairPermission { + @Column(name = "api_keypair_id") + private long apiKeyPairId; + + @Column(name = "sort_order") + private long sortOrder = 0; + + public ApiKeyPairPermissionVO(long apiKeyPairId, String rule, Permission permission, String description) { + super(rule, permission, description); + this.apiKeyPairId = apiKeyPairId; + } + + public long getApiKeyPairId() { + return this.apiKeyPairId; + } + + public void setApiKeyPairId(long keyPairId) { + this.apiKeyPairId = keyPairId; + } + + public ApiKeyPairPermissionVO() { + } + + public void setSortOrder(long sortOrder) { + this.sortOrder = sortOrder; + } + + public long getSortOrder() { + return sortOrder; + } +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/acl/ApiKeyPairVO.java b/engine/schema/src/main/java/org/apache/cloudstack/acl/ApiKeyPairVO.java new file mode 100644 index 000000000000..9f159d4cf69f --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/acl/ApiKeyPairVO.java @@ -0,0 +1,244 @@ +// 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 org.apache.cloudstack.acl; + +import com.cloud.exception.PermissionDeniedException; +import com.cloud.user.Account; +import com.cloud.utils.db.Encrypt; +import java.time.Instant; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPair; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; +import java.util.Date; +import java.util.Objects; +import java.util.UUID; +import org.joda.time.DateTime; + +@Entity +@Table(name = "api_keypair") +public class ApiKeyPairVO implements ApiKeyPair { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id", nullable = false) + private Long id; + + @Column(name = "uuid", nullable = false) + private String uuid = UUID.randomUUID().toString(); + + @Column(name = "user_id", nullable = false) + private Long userId; + + @Column(name = "name", nullable = false) + private String name; + + @Column(name = "domain_id", nullable = false) + private Long domainId; + + @Column(name = "account_id", nullable = false) + private Long accountId; + + @Column(name = "start_date") + @Temporal(value = TemporalType.TIMESTAMP) + private Date startDate; + + @Column(name = "end_date") + @Temporal(value = TemporalType.TIMESTAMP) + private Date endDate; + + @Column(name = "created", nullable = false) + @Temporal(value = TemporalType.TIMESTAMP) + private Date created = Date.from(Instant.now()); + + @Column(name = "description") + private String description = ""; + + @Column(name = "api_key", nullable = false) + private String apiKey; + + @Encrypt + @Column(name = "secret_key", nullable = false) + private String secretKey; + + @Column(name = "removed") + @Temporal(value = TemporalType.TIMESTAMP) + private Date removed; + + public ApiKeyPairVO() { + } + + public ApiKeyPairVO(Long id) { + this.id = id; + } + + public ApiKeyPairVO(Long userId, String description, Date startDate, Date endDate, + String apiKey, String secretKey) { + this.userId = userId; + this.description = description; + this.startDate = startDate; + this.endDate = endDate; + this.apiKey = apiKey; + this.secretKey = secretKey; + } + + public ApiKeyPairVO(String name, Long userId, String description, Date startDate, Date endDate, Account account) { + this.name = Objects.requireNonNullElseGet(name, () -> userId + " - API Keypair"); + this.userId = userId; + this.description = description; + this.startDate = startDate; + this.endDate = endDate; + this.domainId = account.getDomainId(); + this.accountId = account.getAccountId(); + } + + public ApiKeyPairVO(Long id, Long userId) { + this.id = id; + this.userId = userId; + } + + public void validateDate() throws PermissionDeniedException { + Date now = DateTime.now().toDate(); + Date keypairStart = this.getStartDate(); + Date keypairExpiration = this.getEndDate(); + if (keypairStart != null && now.compareTo(keypairStart) <= 0) { + throw new PermissionDeniedException(String.format("Keypair is not valid yet, start date: %s", keypairStart)); + } + if (keypairExpiration != null && now.compareTo(keypairExpiration) >= 0) { + throw new PermissionDeniedException(String.format("Keypair is expired, expiration date: %s", keypairExpiration)); + } + } + + public boolean hasEndDatePassed() { + Date now = DateTime.now().toDate(); + Date keypairExpiration = this.getEndDate(); + return keypairExpiration != null && now.compareTo(keypairExpiration) >= 0; + } + + public long getId() { + return id; + } + + public String getUuid() { + return uuid; + } + + public Long getUserId() { + return userId; + } + + public Date getStartDate() { + return startDate; + } + + public Date getEndDate() { + return endDate; + } + + public Date getCreated() { + return created; + } + + public String getDescription() { + return description; + } + + public String getApiKey() { + return apiKey; + } + + public String getSecretKey() { + return secretKey; + } + + public Class getEntityType() { + return ApiKeyPair.class; + } + + public String getName() { + return name; + } + + public Date getRemoved() { + return removed; + } + + @Override + public long getDomainId() { + return this.domainId; + } + + @Override + public long getAccountId() { + return this.accountId; + } + + public void setId(Long id) { this.id = id; } + + public void setUuid(String uuid) { + this.uuid = uuid; + } + + public void setUserId(Long userId) { + this.userId = userId; + } + + public void setStartDate(Date startDate) { + this.startDate = startDate; + } + + public void setEndDate(Date endDate) { + this.endDate = endDate; + } + + public void setDescription(String description) { + this.description = description; + } + + public void setApiKey(String apiKey) { + this.apiKey = apiKey; + } + + public void setSecretKey(String secretKey) { + this.secretKey = secretKey; + } + + public void setName(String name) { + this.name = name; + } + + public void setDomainId(Long domainId) { + this.domainId = domainId; + } + + public void setAccountId(Long accountId) { + this.accountId = accountId; + } + + public void setCreated(Date created) { + this.created = created; + } + + public void setRemoved(Date removed) { + this.removed = removed; + } +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/acl/dao/ApiKeyPairDao.java b/engine/schema/src/main/java/org/apache/cloudstack/acl/dao/ApiKeyPairDao.java new file mode 100644 index 000000000000..006c2afbc96e --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/acl/dao/ApiKeyPairDao.java @@ -0,0 +1,38 @@ +// 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 org.apache.cloudstack.acl.dao; + +import com.cloud.utils.Pair; +import java.util.List; +import org.apache.cloudstack.acl.ApiKeyPairVO; +import com.cloud.utils.db.GenericDao; +import org.apache.cloudstack.api.command.admin.user.ListUserKeysCmd; + +public interface ApiKeyPairDao extends GenericDao { + ApiKeyPairVO findBySecretKey(String secretKey); + + ApiKeyPairVO findByApiKey(String apiKey); + + ApiKeyPairVO findByUuid(String uuid); + + Pair, Integer> listApiKeysByUserOrApiKeyId(Long userId, Long apiKeyId); + + ApiKeyPairVO getLastApiKeyCreatedByUser(Long userId); + + Pair, Integer> listByUserIdsPaginated(List userIds, ListUserKeysCmd cmd); + +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/acl/dao/ApiKeyPairDaoImpl.java b/engine/schema/src/main/java/org/apache/cloudstack/acl/dao/ApiKeyPairDaoImpl.java new file mode 100644 index 000000000000..6531267a67c8 --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/acl/dao/ApiKeyPairDaoImpl.java @@ -0,0 +1,117 @@ +// 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 org.apache.cloudstack.acl.dao; + +import com.cloud.utils.Pair; +import com.cloud.utils.db.Filter; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import java.util.List; +import org.apache.cloudstack.acl.ApiKeyPairVO; +import org.apache.cloudstack.api.command.admin.user.ListUserKeysCmd; +import org.apache.commons.collections.CollectionUtils; +import org.springframework.stereotype.Component; + +@Component +public class ApiKeyPairDaoImpl extends GenericDaoBase implements ApiKeyPairDao { + + private final SearchBuilder keyPairSearch; + + ApiKeyPairDaoImpl() { + super(); + + keyPairSearch = createSearchBuilder(); + keyPairSearch.and("apiKey", keyPairSearch.entity().getApiKey(), SearchCriteria.Op.EQ); + keyPairSearch.and("secretKey", keyPairSearch.entity().getSecretKey(), SearchCriteria.Op.EQ); + keyPairSearch.and("id", keyPairSearch.entity().getId(), SearchCriteria.Op.EQ); + keyPairSearch.and("userId", keyPairSearch.entity().getUserId(), SearchCriteria.Op.IN); + keyPairSearch.done(); + } + + @Override + public ApiKeyPairVO findByApiKey(String apiKey) { + SearchCriteria sc = keyPairSearch.create(); + sc.setParameters("apiKey", apiKey); + return findOneBy(sc); + } + + public ApiKeyPairVO findBySecretKey(String secretKey) { + SearchCriteria sc = keyPairSearch.create(); + sc.setParameters("secretKey", secretKey); + return findOneBy(sc); + } + + public Pair, Integer> listApiKeysByUserOrApiKeyId(Long userId, Long apiKeyId) { + SearchCriteria sc = keyPairSearch.create(); + if (userId != null) { + sc.setParametersIfNotNull("userId", String.valueOf(userId)); + } + sc.setParametersIfNotNull("id", apiKeyId); + final Filter searchFilter = new Filter(100); + return searchAndCount(sc, searchFilter); + } + + public ApiKeyPairVO getLastApiKeyCreatedByUser(Long userId) { + final SearchCriteria sc = keyPairSearch.create(); + if (userId != null) { + sc.setParameters("userId", String.valueOf(userId)); + } + final Filter searchBySorted = new Filter(ApiKeyPairVO.class, "id", false, null, null); + final List apiKeyPairVOList = listBy(sc, searchBySorted); + if (CollectionUtils.isEmpty(apiKeyPairVOList)) { + return null; + } + return apiKeyPairVOList.get(0); + } + + public Pair, Integer> listByUserIdsPaginated(List userIds, ListUserKeysCmd cmd) { + Long pageSizeVal = cmd.getPageSizeVal(); + Long startIndex = cmd.getStartIndex(); + Filter searchFilter = new Filter(ApiKeyPairVO.class, "id", true, startIndex, pageSizeVal); + + final SearchCriteria sc = keyPairSearch.create(); + sc.setParameters("userId", (Object[]) userIds.toArray(new Long[0])); + + Pair, Integer> apiKeyPairVOList = searchAndCount(sc, searchFilter); + if (CollectionUtils.isEmpty(apiKeyPairVOList.first())) { + return new Pair(List.of(), 0); + } + return apiKeyPairVOList; + } + + + @Override + public boolean update(Long id, ApiKeyPairVO apiKeyPair) { + ApiKeyPairVO ub = createForUpdate(); + + ub.setUuid(apiKeyPair.getUuid()); + ub.setUserId(apiKeyPair.getUserId()); + ub.setName(apiKeyPair.getName()); + ub.setDomainId(apiKeyPair.getDomainId()); + ub.setAccountId(apiKeyPair.getAccountId()); + ub.setStartDate(apiKeyPair.getStartDate()); + ub.setEndDate(apiKeyPair.getEndDate()); + ub.setCreated(apiKeyPair.getCreated()); + ub.setDescription(apiKeyPair.getDescription()); + ub.setApiKey(apiKeyPair.getApiKey()); + ub.setSecretKey(apiKeyPair.getSecretKey()); + ub.setRemoved(apiKeyPair.getRemoved()); + + return super.update(id, ub); + } +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/acl/dao/ApiKeyPairPermissionsDao.java b/engine/schema/src/main/java/org/apache/cloudstack/acl/dao/ApiKeyPairPermissionsDao.java new file mode 100644 index 000000000000..cbca2fd72747 --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/acl/dao/ApiKeyPairPermissionsDao.java @@ -0,0 +1,28 @@ +// 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 org.apache.cloudstack.acl.dao; + +import com.cloud.utils.db.GenericDao; +import org.apache.cloudstack.acl.ApiKeyPairPermissionVO; + +import java.util.List; + +public interface ApiKeyPairPermissionsDao extends GenericDao { + List findAllByApiKeyPairId(Long apiKeyPairId); + + List findAllByKeyPairIdSorted(Long apiKeyPairId); +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/acl/dao/ApiKeyPairPermissionsDaoImpl.java b/engine/schema/src/main/java/org/apache/cloudstack/acl/dao/ApiKeyPairPermissionsDaoImpl.java new file mode 100644 index 000000000000..d8d12ab6fd2c --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/acl/dao/ApiKeyPairPermissionsDaoImpl.java @@ -0,0 +1,67 @@ +// 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 org.apache.cloudstack.acl.dao; + +import com.cloud.utils.db.Filter; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import java.util.Collections; +import java.util.Objects; +import org.apache.cloudstack.acl.ApiKeyPairPermissionVO; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +public class ApiKeyPairPermissionsDaoImpl extends GenericDaoBase implements ApiKeyPairPermissionsDao { + private final SearchBuilder permissionByApiKeyPairIdSearch; + + public ApiKeyPairPermissionsDaoImpl() { + super(); + + permissionByApiKeyPairIdSearch = createSearchBuilder(); + permissionByApiKeyPairIdSearch.and("apiKeyPairId", permissionByApiKeyPairIdSearch.entity().getApiKeyPairId(), SearchCriteria.Op.EQ); + permissionByApiKeyPairIdSearch.done(); + } + + public List findAllByApiKeyPairId(Long apiKeyPairId) { + SearchCriteria sc = permissionByApiKeyPairIdSearch.create(); + sc.setParameters("apiKeyPairId", String.valueOf(apiKeyPairId)); + return listBy(sc); + } + + @Override + public ApiKeyPairPermissionVO persist(final ApiKeyPairPermissionVO item) { + item.setSortOrder(0); + final List permissionsList = findAllByKeyPairIdSorted(item.getApiKeyPairId()); + if (permissionsList != null && !permissionsList.isEmpty()) { + ApiKeyPairPermissionVO lastPermission = permissionsList.get(permissionsList.size() - 1); + item.setSortOrder(lastPermission.getSortOrder() + 1); + } + return super.persist(item); + } + + @Override + public List findAllByKeyPairIdSorted(Long apiKeyPairId) { + final SearchCriteria sc = permissionByApiKeyPairIdSearch.create(); + sc.setParameters("apiKeyPairId", apiKeyPairId); + final Filter searchBySorted = new Filter(ApiKeyPairPermissionVO.class, "sortOrder", true, null, null); + final List apiKeyPairPermissionList = listBy(sc, searchBySorted); + return Objects.requireNonNullElse(apiKeyPairPermissionList, Collections.emptyList()); + } +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/ImageStoreDetailsDaoImpl.java b/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/ImageStoreDetailsDaoImpl.java index ec40dc0dd683..d7e88bd31c3f 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/ImageStoreDetailsDaoImpl.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/ImageStoreDetailsDaoImpl.java @@ -77,7 +77,7 @@ public Map getDetails(long storeId) { for (ImageStoreDetailVO detail : details) { String name = detail.getName(); String value = detail.getValue(); - if (name.equals(ApiConstants.KEY) || name.equals(ApiConstants.S3_SECRET_KEY)) { + if (name.equals(ApiConstants.KEY) || name.equals(ApiConstants.SECRET_KEY)) { value = DBEncryptionUtil.decrypt(value); } detailsMap.put(name, value); diff --git a/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml b/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml index d05635f4614c..f130b7f995e7 100644 --- a/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml +++ b/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml @@ -301,4 +301,6 @@ + + diff --git a/engine/schema/src/main/resources/META-INF/db/schema-42010to42100.sql b/engine/schema/src/main/resources/META-INF/db/schema-42010to42100.sql index b6747ca60716..4d864859acc1 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-42010to42100.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-42010to42100.sql @@ -80,3 +80,37 @@ CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.host', 'storage_access_groups', 'var CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.cluster', 'storage_access_groups', 'varchar(255) DEFAULT NULL COMMENT "storage access groups for the hosts in the cluster"'); CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.host_pod_ref', 'storage_access_groups', 'varchar(255) DEFAULT NULL COMMENT "storage access groups for the hosts in the pod"'); CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.data_center', 'storage_access_groups', 'varchar(255) DEFAULT NULL COMMENT "storage access groups for the hosts in the zone"'); + + +-- Create user token_keypairs table for apikey/secretkey tokens +CREATE TABLE IF NOT EXISTS `cloud`.`api_keypair` ( + `id` bigint(20) unsigned NOT NULL auto_increment, + `uuid` varchar(40) UNIQUE NOT NULL, + `name` varchar(255) NOT NULL, + `domain_id` bigint(20) unsigned NOT NULL, + `account_id` bigint(20) unsigned NOT NULL, + `user_id` bigint(20) unsigned NOT NULL, + `start_date` datetime, + `end_date` datetime, + `description` varchar(100), + `api_key` varchar(255) NOT NULL, + `secret_key` varchar(255) NOT NULL, + `created` datetime NOT NULL, + `removed` datetime, + PRIMARY KEY (`id`), + CONSTRAINT `fk_api_keypair__user_id` FOREIGN KEY(`user_id`) REFERENCES `cloud`.`user`(`id`), + CONSTRAINT `fk_api_keypair__account_id` FOREIGN KEY(`account_id`) REFERENCES `cloud`.`account`(`id`), + CONSTRAINT `fk_api_keypair__domain_id` FOREIGN KEY(`domain_id`) REFERENCES `cloud`.`domain`(`id`) + ); + +CREATE TABLE IF NOT EXISTS `cloud`.`keypair_permissions` ( + `id` bigint(20) unsigned NOT NULL auto_increment, + `uuid` varchar(40) UNIQUE, + `sort_order` bigint(20) unsigned NOT NULL DEFAULT 0, + `rule` varchar(255) NOT NULL, + `api_keypair_id` bigint(20) unsigned NOT NULL, + `permission` varchar(255) NOT NULL, + `description` varchar(255), + PRIMARY KEY (`id`), + CONSTRAINT `fk_keypair_permissions__api_keypair_id` FOREIGN KEY(`api_keypair_id`) REFERENCES `cloud`.`api_keypair`(`id`) + ); 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 340cfa9055fb..dcba71e10985 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 @@ -29,8 +29,6 @@ select user.lastname, user.email, user.state, - user.api_key, - user.secret_key, user.created, user.removed, user.timezone, diff --git a/engine/storage/src/main/java/org/apache/cloudstack/storage/image/datastore/ImageStoreHelper.java b/engine/storage/src/main/java/org/apache/cloudstack/storage/image/datastore/ImageStoreHelper.java index dbb606b44a8a..51edd62326dd 100644 --- a/engine/storage/src/main/java/org/apache/cloudstack/storage/image/datastore/ImageStoreHelper.java +++ b/engine/storage/src/main/java/org/apache/cloudstack/storage/image/datastore/ImageStoreHelper.java @@ -131,7 +131,7 @@ public ImageStoreVO createImageStore(Map params, Map getApisAllowedToUser(Role role, User user, List apiN } List allPermissions = roleService.findAllPermissionsBy(role.getId()); + List allPermissionEntities = allPermissions.stream().map(permission -> (RolePermissionEntity) permission) + .collect(Collectors.toList()); + List allowedApis = new ArrayList<>(); for (String api : apiNames) { - if (checkApiPermissionByRole(role, api, allPermissions)) { + if (checkApiPermissionByRole(role, api, allPermissionEntities, false)) { allowedApis.add(api); } } @@ -84,8 +90,8 @@ public List getApisAllowedToUser(Role role, User user, List apiN * @param allPermissions list of role permissions for the given role * @return if the role has the permission for the API */ - public boolean checkApiPermissionByRole(Role role, String apiName, List allPermissions) { - for (final RolePermission permission : allPermissions) { + public boolean checkApiPermissionByRole(Role role, String apiName, List allPermissions, boolean keyPairOverride) { + for (RolePermissionEntity permission : allPermissions) { if (!permission.getRule().matches(apiName)) { continue; } @@ -99,8 +105,10 @@ public boolean checkApiPermissionByRole(Role role, String apiName, List> roleAndPermissions = getRolePermissionsUsingCache(account.getRoleId()); final Role accountRole = roleAndPermissions.first(); if (accountRole == null) { throw new PermissionDeniedException(String.format("The account [%s] has role null or unknown.", account)); } - if (accountRole.getRoleType() == RoleType.Admin && accountRole.getId() == RoleType.Admin.getId()) { + if (accountRole.getRoleType() == RoleType.Admin && accountRole.getId() == RoleType.Admin.getId() && apiKeyPairPermissions.length == 0) { if (logger.isTraceEnabled()) { logger.trace(String.format("Account [%s] is Root Admin or Domain Admin, all APIs are allowed.", account)); } return true; } - List allPermissions = roleService.findAllPermissionsBy(accountRole.getId()); - if (checkApiPermissionByRole(accountRole, commandName, allPermissions)) { + List allRules = defineNewKeypairRules(accountRole, apiKeyPairPermissions); + + boolean override = apiKeyPairPermissions.length != 0; + + if (checkApiPermissionByRole(accountRole, commandName, allRules, override)) { return true; } - throw new UnavailableCommandException(String.format("The API [%s] does not exist or is not available for the account %s.", commandName, account)); + throw new UnavailableCommandException(String.format("The API [%s] does not exist or is not available for the account %s.", commandName, account.getAccountName())); + } + + public List defineNewKeypairRules(Role accountRole, ApiKeyPairPermission ... apiKeyPairPermissions) { + List allPermissions; + if (apiKeyPairPermissions.length == 0) { + List allRolePermissions = roleService.findAllPermissionsBy(accountRole.getId()); + allPermissions = allRolePermissions.stream().map(permission -> (RolePermissionEntity) permission) + .collect(Collectors.toList()); + } else { + allPermissions = Arrays.asList(apiKeyPairPermissions); + } + return allPermissions; } /** diff --git a/plugins/acl/dynamic-role-based/src/test/java/org/apache/cloudstack/acl/DynamicRoleBasedAPIAccessCheckerTest.java b/plugins/acl/dynamic-role-based/src/test/java/org/apache/cloudstack/acl/DynamicRoleBasedAPIAccessCheckerTest.java index e58be3a75e79..64d264a68e13 100644 --- a/plugins/acl/dynamic-role-based/src/test/java/org/apache/cloudstack/acl/DynamicRoleBasedAPIAccessCheckerTest.java +++ b/plugins/acl/dynamic-role-based/src/test/java/org/apache/cloudstack/acl/DynamicRoleBasedAPIAccessCheckerTest.java @@ -22,6 +22,8 @@ import java.util.Collections; import java.util.List; +import com.cloud.exception.UnavailableCommandException; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPairPermission; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -195,4 +197,68 @@ public void getApisAllowedToUserTestPermissionDenyForGivenApiShouldReturnEmptyLi List apisReceived = apiAccessCheckerSpy.getApisAllowedToUser(getTestRole(), getTestUser(), apiNames); Assert.assertEquals(0, apisReceived.size()); } + + @Test(expected = UnavailableCommandException.class) + public void checkAccessTestInvalidApiKeyPairPermission() { + final String api = "someDeniedApi"; + final ApiKeyPairPermission permission = new ApiKeyPairPermissionVO(1L, api, Permission.DENY, null); + assertFalse(apiAccessCheckerSpy.checkAccess(getTestUser(), api, permission)); + } + + @Test(expected = UnavailableCommandException.class) + public void checkAccessTestUnrelatedApiKeyPairPermission() { + final String api = "someDeniedApi"; + final ApiKeyPairPermission permission = new ApiKeyPairPermissionVO(1L, "apiName", Permission.ALLOW, null); + assertFalse(apiAccessCheckerSpy.checkAccess(getTestUser(), api, permission)); + } + + @Test + public void checkAccessTestValidApiKeyPairPermission() { + final String api = "someAllowedApi"; + final ApiKeyPairPermission permission = new ApiKeyPairPermissionVO(1L, api, Permission.ALLOW, null); + assertTrue(apiAccessCheckerSpy.checkAccess(getTestUser(), api, permission)); + } + + @Test + public void checkAccessTestValidMultipleApiKeyPermissions() { + final String api = "someAllowedApi"; + final ApiKeyPairPermission[] permissions = new ApiKeyPairPermission[]{ + new ApiKeyPairPermissionVO(1L, "someDeniedApi", Permission.DENY, null), + new ApiKeyPairPermissionVO(1L, api, Permission.ALLOW, null) + }; + assertTrue(apiAccessCheckerSpy.checkAccess(getTestUser(), api, permissions)); + } + + @Test(expected = UnavailableCommandException.class) + public void checkAccessTestInvalidMultipleApiKeyPermissions() { + final String api = "someDeniedApi"; + final ApiKeyPairPermission[] permissions = new ApiKeyPairPermission[]{ + new ApiKeyPairPermissionVO(1L, "someAllowedApi", Permission.ALLOW, null), + new ApiKeyPairPermissionVO(1L, api, Permission.DENY, null) + }; + assertFalse(apiAccessCheckerSpy.checkAccess(getTestUser(), api, permissions)); + } + + + @Test + public void checkAccessTestValidApiKeyPairPermissionWithNullOverride() { + final String api = "someAllowedApi"; + final ApiKeyPairPermission[] emptyPermissionArray = List.of().toArray(new ApiKeyPairPermission[0]); + final RolePermission permission = new RolePermissionVO(1L, api, Permission.ALLOW, null); + Mockito.doReturn(Collections.singletonList(permission)).when(roleServiceMock).findAllPermissionsBy(Mockito.anyLong()); + + assertTrue(apiAccessCheckerSpy.checkAccess(getTestUser(), api, emptyPermissionArray)); + Mockito.verify(roleServiceMock, Mockito.times(1)).findAllPermissionsBy(Mockito.anyLong()); + } + + @Test(expected = UnavailableCommandException.class) + public void checkAccessTestInvalidApiKeyPairPermissionWithNullOverride() { + final String api = "someDeniedApi"; + final ApiKeyPairPermission[] emptyPermissionArray = List.of().toArray(new ApiKeyPairPermission[0]); + final RolePermission permission = new RolePermissionVO(1L, api, Permission.DENY, null); + Mockito.doReturn(Collections.singletonList(permission)).when(roleServiceMock).findAllPermissionsBy(Mockito.anyLong()); + + assertTrue(apiAccessCheckerSpy.checkAccess(getTestUser(), api, emptyPermissionArray)); + Mockito.verify(roleServiceMock, Mockito.times(1)).findAllPermissionsBy(Mockito.anyLong()); + } } diff --git a/plugins/acl/project-role-based/src/main/java/org/apache/cloudstack/acl/ProjectRoleBasedApiAccessChecker.java b/plugins/acl/project-role-based/src/main/java/org/apache/cloudstack/acl/ProjectRoleBasedApiAccessChecker.java index 2e7ae23d6f1b..b2d347543bf6 100644 --- a/plugins/acl/project-role-based/src/main/java/org/apache/cloudstack/acl/ProjectRoleBasedApiAccessChecker.java +++ b/plugins/acl/project-role-based/src/main/java/org/apache/cloudstack/acl/ProjectRoleBasedApiAccessChecker.java @@ -23,6 +23,7 @@ import javax.naming.ConfigurationException; import org.apache.cloudstack.acl.RolePermissionEntity.Permission; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPairPermission; import org.apache.cloudstack.context.CallContext; import com.cloud.exception.PermissionDeniedException; @@ -105,7 +106,7 @@ public List getApisAllowedToUser(Role role, User user, List apiN } @Override - public boolean checkAccess(User user, String apiCommandName) throws PermissionDeniedException { + public boolean checkAccess(User user, String apiCommandName, ApiKeyPairPermission... apiKeyPairPermissions) throws PermissionDeniedException { if (!isEnabled()) { return true; } @@ -150,7 +151,7 @@ public boolean checkAccess(User user, String apiCommandName) throws PermissionDe } @Override - public boolean checkAccess(Account account, String apiCommandName) throws PermissionDeniedException { + public boolean checkAccess(Account account, String apiCommandName, ApiKeyPairPermission... apiKeyPairPermissions) throws PermissionDeniedException { return true; } diff --git a/plugins/acl/static-role-based/src/main/java/org/apache/cloudstack/acl/StaticRoleBasedAPIAccessChecker.java b/plugins/acl/static-role-based/src/main/java/org/apache/cloudstack/acl/StaticRoleBasedAPIAccessChecker.java index 3444f967d784..90a70b1bb4a6 100644 --- a/plugins/acl/static-role-based/src/main/java/org/apache/cloudstack/acl/StaticRoleBasedAPIAccessChecker.java +++ b/plugins/acl/static-role-based/src/main/java/org/apache/cloudstack/acl/StaticRoleBasedAPIAccessChecker.java @@ -26,6 +26,7 @@ import javax.naming.ConfigurationException; import com.cloud.exception.UnavailableCommandException; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPairPermission; import org.apache.cloudstack.api.APICommand; @@ -90,7 +91,7 @@ public List getApisAllowedToUser(Role role, User user, List apiN } @Override - public boolean checkAccess(User user, String commandName) throws PermissionDeniedException { + public boolean checkAccess(User user, String commandName, ApiKeyPairPermission... apiKeyPairPermissions) throws PermissionDeniedException { if (!isEnabled()) { return true; } @@ -104,7 +105,7 @@ public boolean checkAccess(User user, String commandName) throws PermissionDenie } @Override - public boolean checkAccess(Account account, String commandName) { + public boolean checkAccess(Account account, String commandName, ApiKeyPairPermission... apiKeyPairPermissions) { if (!isEnabled()) { return true; } diff --git a/plugins/api/discovery/pom.xml b/plugins/api/discovery/pom.xml index e947834c3fad..6ea4af008df0 100644 --- a/plugins/api/discovery/pom.xml +++ b/plugins/api/discovery/pom.xml @@ -39,6 +39,12 @@ cloud-utils ${project.version} + + org.apache.cloudstack + cloud-plugin-api-limit-account-based + ${project.version} + compile + diff --git a/plugins/api/discovery/src/main/java/org/apache/cloudstack/api/command/user/discovery/ListApisCmd.java b/plugins/api/discovery/src/main/java/org/apache/cloudstack/api/command/user/discovery/ListApisCmd.java index aa78a725a34f..783ca119499d 100644 --- a/plugins/api/discovery/src/main/java/org/apache/cloudstack/api/command/user/discovery/ListApisCmd.java +++ b/plugins/api/discovery/src/main/java/org/apache/cloudstack/api/command/user/discovery/ListApisCmd.java @@ -52,7 +52,7 @@ public class ListApisCmd extends BaseCmd { public void execute() throws ServerApiException { if (_apiDiscoveryService != null) { User user = CallContext.current().getCallingUser(); - ListResponse response = (ListResponse)_apiDiscoveryService.listApis(user, name); + ListResponse response = (ListResponse)_apiDiscoveryService.listApis(user, name, this); if (response == null) { throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Api Discovery plugin was unable to find an api by that name or process any apis"); } diff --git a/plugins/api/discovery/src/main/java/org/apache/cloudstack/discovery/ApiDiscoveryService.java b/plugins/api/discovery/src/main/java/org/apache/cloudstack/discovery/ApiDiscoveryService.java index 5a6eab7389d1..073010a8eb60 100644 --- a/plugins/api/discovery/src/main/java/org/apache/cloudstack/discovery/ApiDiscoveryService.java +++ b/plugins/api/discovery/src/main/java/org/apache/cloudstack/discovery/ApiDiscoveryService.java @@ -18,6 +18,7 @@ import com.cloud.user.Account; import org.apache.cloudstack.api.BaseResponse; +import org.apache.cloudstack.api.command.user.discovery.ListApisCmd; import org.apache.cloudstack.api.response.ListResponse; import com.cloud.user.User; @@ -28,5 +29,5 @@ public interface ApiDiscoveryService extends PluggableService { List listApiNames(Account account); - ListResponse listApis(User user, String apiName); + ListResponse listApis(User user, String apiName, ListApisCmd cmd); } diff --git a/plugins/api/discovery/src/main/java/org/apache/cloudstack/discovery/ApiDiscoveryServiceImpl.java b/plugins/api/discovery/src/main/java/org/apache/cloudstack/discovery/ApiDiscoveryServiceImpl.java index 452b95cf2c05..393f32368524 100644 --- a/plugins/api/discovery/src/main/java/org/apache/cloudstack/discovery/ApiDiscoveryServiceImpl.java +++ b/plugins/api/discovery/src/main/java/org/apache/cloudstack/discovery/ApiDiscoveryServiceImpl.java @@ -26,6 +26,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; import javax.inject.Inject; @@ -33,6 +34,9 @@ import org.apache.cloudstack.acl.Role; import org.apache.cloudstack.acl.RoleService; import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.acl.RolePermissionEntity; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPair; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPairService; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.BaseAsyncCmd; import org.apache.cloudstack.api.BaseAsyncCreateCmd; @@ -44,6 +48,7 @@ import org.apache.cloudstack.api.response.ApiParameterResponse; import org.apache.cloudstack.api.response.ApiResponseResponse; import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.ratelimit.ApiRateLimitService; import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang3.StringUtils; @@ -73,6 +78,9 @@ public class ApiDiscoveryServiceImpl extends ComponentLifecycleBase implements A @Inject RoleService roleService; + @Inject + ApiKeyPairService apiKeyPairService; + protected ApiDiscoveryServiceImpl() { super(); } @@ -246,16 +254,25 @@ public List listApiNames(Account account) { } @Override - public ListResponse listApis(User user, String name) { + public ListResponse listApis(User user, String name, ListApisCmd cmd) { ListResponse response = new ListResponse<>(); List responseList = new ArrayList<>(); List apisAllowed = new ArrayList<>(s_apiNameDiscoveryResponseMap.keySet()); + String apikey = accountService.getAccessingApiKey(cmd); if (user == null) return null; + Account account = accountService.getAccount(user.getAccountId()); - if (name != null) { + if (account == null) { + throw new PermissionDeniedException(String.format("The account with id [%s] for user [%s] is null.", user.getAccountId(), user)); + } + + Role role = roleService.findRole(account.getRoleId()); + if (apikey != null) { + responseList = listApisForKeyPair(apikey, name, account, user, role, apisAllowed); + } else if (name != null) { if (!s_apiNameDiscoveryResponseMap.containsKey(name)) return null; @@ -267,14 +284,9 @@ public ListResponse listApis(User user, String name) { return null; } } - responseList.add(getApiDiscoveryResponseWithAccessibleParams(name, account)); + responseList.add(s_apiNameDiscoveryResponseMap.get(name)); } else { - if (account == null) { - throw new PermissionDeniedException(String.format("The account with id [%s] for user [%s] is null.", user.getAccountId(), user)); - } - - final Role role = roleService.findRole(account.getRoleId()); if (role == null || role.getId() < 1L) { throw new PermissionDeniedException(String.format("The account [%s] has role null or unknown.", ReflectionToStringBuilderUtils.reflectOnlySelectedFields(account, "accountName", "uuid"))); @@ -290,7 +302,7 @@ public ListResponse listApis(User user, String name) { } for (String apiName: apisAllowed) { - responseList.add(getApiDiscoveryResponseWithAccessibleParams(apiName, account)); + responseList.add(s_apiNameDiscoveryResponseMap.get(apiName)); } } response.setResponses(responseList); @@ -324,6 +336,47 @@ public List> getCommands() { return cmdList; } + protected List listApisForKeyPair(String apiKey, String apiName, Account account, User user, Role role, List apisAllowed) { + ApiKeyPair keyPair = accountService.getKeyPairByApiKey(apiKey); + List rolePermissionEntities = apiKeyPairService.findAllPermissionsByKeyPairId(keyPair.getId(), account.getRoleId()).stream() + .map(apiKeyPairPermission -> (RolePermissionEntity) apiKeyPairPermission).collect(Collectors.toList()); + + List filteredApis = new ArrayList<>(); + if (apiName != null && isApiAllowedForKey(rolePermissionEntities, apiName)) { + filteredApis = List.of(apiName); + } else { + for (String api : apisAllowed) { + if (isApiAllowedForKey(rolePermissionEntities, api)) { + filteredApis.add(api); + } + } + } + + checkRateLimit(user, role, filteredApis); + + return filteredApis.stream().map(api -> s_apiNameDiscoveryResponseMap.get(api)).collect(Collectors.toList()); + } + + protected boolean isApiAllowedForKey(List rolePermissionEntities, String apiName) { + for (RolePermissionEntity rolePermissionEntity : rolePermissionEntities) { + if (!rolePermissionEntity.getRule().matches(apiName)) { + continue; + } + return rolePermissionEntity.getPermission().equals(RolePermissionEntity.Permission.ALLOW); + } + return false; + } + + private void checkRateLimit(User user, Role role, List apiNames) { + for (APIChecker apiChecker : _apiAccessCheckers) { + if (!(apiChecker instanceof ApiRateLimitService)) { + continue; + } + apiChecker.getApisAllowedToUser(role, user, apiNames); + return; + } + } + public List getApiAccessCheckers() { return _apiAccessCheckers; } diff --git a/plugins/api/discovery/src/test/java/org/apache/cloudstack/discovery/ApiDiscoveryTest.java b/plugins/api/discovery/src/test/java/org/apache/cloudstack/discovery/ApiDiscoveryTest.java index eea78d8abb93..2a6d3bd85c3f 100644 --- a/plugins/api/discovery/src/test/java/org/apache/cloudstack/discovery/ApiDiscoveryTest.java +++ b/plugins/api/discovery/src/test/java/org/apache/cloudstack/discovery/ApiDiscoveryTest.java @@ -86,7 +86,7 @@ private Account getNormalAccount() { @Test (expected = PermissionDeniedException.class) public void listApisTestThrowPermissionDeniedExceptionOnAccountNull() throws PermissionDeniedException { Mockito.when(accountServiceMock.getAccount(Mockito.anyLong())).thenReturn(null); - discoveryServiceSpy.listApis(getTestUser(), null); + discoveryServiceSpy.listApis(getTestUser(), null, null); } @Test (expected = PermissionDeniedException.class) @@ -94,7 +94,7 @@ public void listApisTestThrowPermissionDeniedExceptionOnRoleNull() throws Permis Mockito.when(accountServiceMock.getAccount(Mockito.anyLong())).thenReturn(getNormalAccount()); Mockito.when(roleServiceMock.findRole(Mockito.anyLong())).thenReturn(null); - discoveryServiceSpy.listApis(getTestUser(), null); + discoveryServiceSpy.listApis(getTestUser(), null, null); } @Test (expected = PermissionDeniedException.class) @@ -104,7 +104,7 @@ public void listApisTestThrowPermissionDeniedExceptionOnRoleUnknown() throws Per Mockito.when(accountServiceMock.getAccount(Mockito.anyLong())).thenReturn(getNormalAccount()); Mockito.when(roleServiceMock.findRole(Mockito.anyLong())).thenReturn(unknownRoleVO); - discoveryServiceSpy.listApis(getTestUser(), null); + discoveryServiceSpy.listApis(getTestUser(), null, null); } @Test @@ -115,7 +115,7 @@ public void listApisTestDoesNotGetApisAllowedToUserOnAdminRole() throws Permissi Mockito.when(accountServiceMock.getAccount(Mockito.anyLong())).thenReturn(adminAccountVO); Mockito.when(roleServiceMock.findRole(Mockito.anyLong())).thenReturn(adminRoleVO); - discoveryServiceSpy.listApis(getTestUser(), null); + discoveryServiceSpy.listApis(getTestUser(), null, null); Mockito.verify(apiCheckerMock, Mockito.times(0)).getApisAllowedToUser(any(Role.class), any(User.class), anyList()); } @@ -127,7 +127,7 @@ public void listApisTestGetsApisAllowedToUserOnUserRole() throws PermissionDenie Mockito.when(accountServiceMock.getAccount(Mockito.anyLong())).thenReturn(getNormalAccount()); Mockito.when(roleServiceMock.findRole(Mockito.anyLong())).thenReturn(userRoleVO); - discoveryServiceSpy.listApis(getTestUser(), null); + discoveryServiceSpy.listApis(getTestUser(), null, null); Mockito.verify(apiCheckerMock, Mockito.times(1)).getApisAllowedToUser(any(Role.class), any(User.class), anyList()); } diff --git a/plugins/api/rate-limit/src/main/java/org/apache/cloudstack/ratelimit/ApiRateLimitServiceImpl.java b/plugins/api/rate-limit/src/main/java/org/apache/cloudstack/ratelimit/ApiRateLimitServiceImpl.java index 917cd7bb2b46..bb435ade816a 100644 --- a/plugins/api/rate-limit/src/main/java/org/apache/cloudstack/ratelimit/ApiRateLimitServiceImpl.java +++ b/plugins/api/rate-limit/src/main/java/org/apache/cloudstack/ratelimit/ApiRateLimitServiceImpl.java @@ -27,6 +27,7 @@ import net.sf.ehcache.CacheManager; import org.apache.cloudstack.acl.Role; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPairPermission; import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; import org.springframework.stereotype.Component; @@ -161,17 +162,17 @@ public void throwExceptionDueToApiRateLimitReached(Long accountId) throws Reques } @Override - public boolean checkAccess(User user, String apiCommandName) throws PermissionDeniedException { + public boolean checkAccess(User user, String apiCommandName, ApiKeyPairPermission ... apiKeyPairPermissions) throws PermissionDeniedException { if (!isEnabled()) { return true; } Account account = _accountService.getAccount(user.getAccountId()); - return checkAccess(account, apiCommandName); + return checkAccess(account, apiCommandName, apiKeyPairPermissions); } @Override - public boolean checkAccess(Account account, String commandName) { + public boolean checkAccess(Account account, String commandName, ApiKeyPairPermission ... apiKeyPairPermissions) { Long accountId = account.getAccountId(); if (_accountService.isRootAdmin(accountId)) { logger.info(String.format("Account [%s] is Root Admin, in this case, API limit does not apply.", diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java index 7a987df0a35b..12254ee12fdf 100644 --- a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java @@ -247,7 +247,7 @@ protected QuotaSummaryResponse getQuotaSummaryResponse(final Account account) { } public boolean isUserAllowedToSeeActivationRules(User user) { - List apiList = (List) apiDiscoveryService.listApis(user, null).getResponses(); + List apiList = (List) apiDiscoveryService.listApis(user, null, null).getResponses(); return apiList.stream().anyMatch(response -> StringUtils.equalsAny(response.getName(), "quotaTariffCreate", "quotaTariffUpdate")); } diff --git a/plugins/database/quota/src/test/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImplTest.java b/plugins/database/quota/src/test/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImplTest.java index 1f5480404e4b..3629bf2e3fe9 100644 --- a/plugins/database/quota/src/test/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImplTest.java +++ b/plugins/database/quota/src/test/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImplTest.java @@ -652,7 +652,7 @@ public void isUserAllowedToSeeActivationRulesTestWithPermissionToCreateTariff() ListResponse responseList = new ListResponse<>(); responseList.setResponses(cmdList); - Mockito.doReturn(responseList).when(discoveryServiceMock).listApis(userMock, null); + Mockito.doReturn(responseList).when(discoveryServiceMock).listApis(userMock, null, null); assertTrue(quotaResponseBuilderSpy.isUserAllowedToSeeActivationRules(userMock)); } @@ -668,7 +668,7 @@ public void isUserAllowedToSeeActivationRulesTestWithPermissionToUpdateTariff() ListResponse responseList = new ListResponse<>(); responseList.setResponses(cmdList); - Mockito.doReturn(responseList).when(discoveryServiceMock).listApis(userMock, null); + Mockito.doReturn(responseList).when(discoveryServiceMock).listApis(userMock, null, null); assertTrue(quotaResponseBuilderSpy.isUserAllowedToSeeActivationRules(userMock)); } @@ -684,7 +684,7 @@ public void isUserAllowedToSeeActivationRulesTestWithNoPermission() { ListResponse responseList = new ListResponse<>(); responseList.setResponses(cmdList); - Mockito.doReturn(responseList).when(discoveryServiceMock).listApis(userMock, null); + Mockito.doReturn(responseList).when(discoveryServiceMock).listApis(userMock, null, null); assertFalse(quotaResponseBuilderSpy.isUserAllowedToSeeActivationRules(userMock)); } diff --git a/plugins/hypervisors/baremetal/src/main/java/com/cloud/baremetal/manager/BaremetalVlanManagerImpl.java b/plugins/hypervisors/baremetal/src/main/java/com/cloud/baremetal/manager/BaremetalVlanManagerImpl.java index 5695325fb137..c05d52326cea 100644 --- a/plugins/hypervisors/baremetal/src/main/java/com/cloud/baremetal/manager/BaremetalVlanManagerImpl.java +++ b/plugins/hypervisors/baremetal/src/main/java/com/cloud/baremetal/manager/BaremetalVlanManagerImpl.java @@ -263,9 +263,8 @@ public boolean start() { user.setSource(User.Source.UNKNOWN); user = userDao.persist(user); - String[] keys = acntMgr.createApiKeyAndSecretKey(user.getId()); - user.setApiKey(keys[0]); - user.setSecretKey(keys[1]); + acntMgr.createApiKeyAndSecretKey(user.getId()); + userDao.update(user.getId(), user); return true; } diff --git a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/KubernetesClusterManagerImpl.java b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/KubernetesClusterManagerImpl.java index f551344ec0e3..c85187d19b9d 100644 --- a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/KubernetesClusterManagerImpl.java +++ b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/KubernetesClusterManagerImpl.java @@ -43,6 +43,7 @@ import com.cloud.uservm.UserVm; import com.cloud.vm.UserVmService; +import org.apache.cloudstack.acl.ApiKeyPairVO; import org.apache.cloudstack.acl.ControlledEntity; import org.apache.cloudstack.acl.Role; import org.apache.cloudstack.acl.RolePermissionEntity; @@ -1463,12 +1464,12 @@ protected String[] getServiceUserKeys(Account owner) { KUBEADMIN_ACCOUNT_NAME, "kubeadmin", null, UUID.randomUUID().toString(), User.Source.UNKNOWN)); keys = createUserApiKeyAndSecretKey(kube.getId()); } else { - String apiKey = kubeadmin.getApiKey(); - String secretKey = kubeadmin.getSecretKey(); - if (StringUtils.isAnyEmpty(apiKey, secretKey)) { + ApiKeyPairVO latestKeypair = ApiDBUtils.searchForLatestUserKeyPair(kubeadmin.getId()); + + if (latestKeypair == null) { keys = createUserApiKeyAndSecretKey(kubeadmin.getId()); } else { - keys = new String[]{apiKey, secretKey}; + keys = new String[]{latestKeypair.getApiKey(), latestKeypair.getSecretKey()}; } } return keys; diff --git a/plugins/network-elements/juniper-contrail/src/test/java/org/apache/cloudstack/network/contrail/management/MockAccountManager.java b/plugins/network-elements/juniper-contrail/src/test/java/org/apache/cloudstack/network/contrail/management/MockAccountManager.java index d30d7b2f74dc..4f8d6371419e 100644 --- a/plugins/network-elements/juniper-contrail/src/test/java/org/apache/cloudstack/network/contrail/management/MockAccountManager.java +++ b/plugins/network-elements/juniper-contrail/src/test/java/org/apache/cloudstack/network/contrail/management/MockAccountManager.java @@ -25,20 +25,28 @@ import javax.naming.ConfigurationException; import com.cloud.api.auth.SetupUserTwoFactorAuthenticationCmd; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPairPermission; +import org.apache.cloudstack.api.BaseCmd; import org.apache.cloudstack.api.command.admin.account.CreateAccountCmd; +import org.apache.cloudstack.api.command.admin.user.DeleteUserCmd; +import org.apache.cloudstack.api.command.admin.user.DeleteUserKeysCmd; import org.apache.cloudstack.api.command.admin.user.GetUserKeysCmd; +import org.apache.cloudstack.api.command.admin.user.ListUserKeyRulesCmd; +import org.apache.cloudstack.api.command.admin.user.ListUserKeysCmd; import org.apache.cloudstack.api.command.admin.user.MoveUserCmd; +import org.apache.cloudstack.api.command.admin.user.RegisterUserKeysCmd; +import org.apache.cloudstack.api.command.admin.user.UpdateUserCmd; import org.apache.cloudstack.api.response.UserTwoFactorAuthenticationSetupResponse; import org.apache.cloudstack.auth.UserTwoFactorAuthenticator; import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPair; import org.apache.cloudstack.acl.ControlledEntity; import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.response.ApiKeyPairResponse; +import org.apache.cloudstack.api.response.ListResponse; 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.api.command.admin.user.RegisterCmd; -import org.apache.cloudstack.api.command.admin.user.UpdateUserCmd; import org.apache.cloudstack.context.CallContext; import com.cloud.api.query.vo.ControlledViewEntity; @@ -118,7 +126,7 @@ public void checkAccess(Account arg0, AccessType arg1, boolean arg2, ControlledE } @Override - public String[] createApiKeyAndSecretKey(RegisterCmd arg0) { + public ApiKeyPair createApiKeyAndSecretKey(RegisterUserKeysCmd arg0) { // TODO Auto-generated method stub return null; } @@ -394,7 +402,7 @@ public UserAccount enableUser(long arg0) { } @Override - public Pair findUserByApiKey(String arg0) { + public Ternary findUserByApiKey(String arg0) { // TODO Auto-generated method stub return null; } @@ -459,6 +467,10 @@ public void validateAccountHasAccessToResource(Account account, AccessType acces // TODO Auto-generated method stub } + @Override + public void validateCallingUserHasAccessToDesiredUser(Long userId) { + } + @Override public Long finalyzeAccountId(String accountName, Long domainId, Long projectId, boolean enabledOnly) { // TODO Auto-generated method stub @@ -491,10 +503,23 @@ public Pair> getKeys(GetUserKeysCmd cmd){ } @Override - public Pair> getKeys(Long userId) { + public ListResponse listKeys(ListUserKeysCmd cmd) { return null; } + @Override + public List listKeyRules(ListUserKeyRulesCmd cmd) { + return null; + } + + @Override + public void deleteApiKey(DeleteUserKeysCmd cmd) { + } + + @Override + public void deleteApiKey(ApiKeyPair id) { + } + @Override public List listUserTwoFactorAuthenticationProviders() { return null; @@ -505,6 +530,26 @@ public UserTwoFactorAuthenticator getUserTwoFactorAuthenticationProvider(Long do return null; } + @Override + public ApiKeyPair getLatestUserKeyPair(Long userId) { + return null; + } + + @Override + public ApiKeyPair getKeyPairById(Long id) { + return null; + } + + @Override + public ApiKeyPair getKeyPairByApiKey(String apiKey) { + return null; + } + + @Override + public String getAccessingApiKey(BaseCmd cmd) { + return null; + } + @Override public void checkAccess(User user, ControlledEntity entity) throws PermissionDeniedException { diff --git a/plugins/network-elements/tungsten/src/main/java/org/apache/cloudstack/network/tungsten/service/TungstenServiceImpl.java b/plugins/network-elements/tungsten/src/main/java/org/apache/cloudstack/network/tungsten/service/TungstenServiceImpl.java index 60b5b7290a90..2a29b3976b01 100644 --- a/plugins/network-elements/tungsten/src/main/java/org/apache/cloudstack/network/tungsten/service/TungstenServiceImpl.java +++ b/plugins/network-elements/tungsten/src/main/java/org/apache/cloudstack/network/tungsten/service/TungstenServiceImpl.java @@ -19,6 +19,7 @@ import com.cloud.agent.AgentManager; import com.cloud.agent.api.Answer; import com.cloud.agent.api.Command; +import com.cloud.api.ApiDBUtils; import com.cloud.configuration.Config; import com.cloud.configuration.ConfigurationManager; import com.cloud.dc.DataCenter; @@ -114,6 +115,7 @@ import net.juniper.tungsten.api.types.VirtualMachine; import net.juniper.tungsten.api.types.VirtualMachineInterface; import net.juniper.tungsten.api.types.VirtualNetwork; +import org.apache.cloudstack.acl.ApiKeyPairVO; import org.apache.cloudstack.api.BaseResponse; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.framework.config.dao.ConfigurationDao; @@ -1182,9 +1184,10 @@ public boolean updateLoadBalancerSsl(Network network, LoadBalancingRule loadBala int listenerPort = NetUtils.HTTPS_PORT; User callerUser = accountMgr.getActiveUser(CallContext.current().getCallingUserId()); - String apiKey = callerUser.getApiKey(); - String secretKey = callerUser.getSecretKey(); - if (apiKey != null && secretKey != null) { + ApiKeyPairVO latestKeypair = ApiDBUtils.searchForLatestUserKeyPair(callerUser.getId()); + if (latestKeypair != null) { + String apiKey = latestKeypair.getApiKey(); + String secretKey = latestKeypair.getSecretKey(); String url; try { String data = "apiKey=" + URLEncoder.encode(apiKey, StandardCharsets.UTF_8.name()).replace("\\+", "%20") + "&command" diff --git a/plugins/network-elements/tungsten/src/test/java/org/apache/cloudstack/network/tungsten/service/TungstenElementTest.java b/plugins/network-elements/tungsten/src/test/java/org/apache/cloudstack/network/tungsten/service/TungstenElementTest.java index 58084d3072d1..26b1d53b87a8 100644 --- a/plugins/network-elements/tungsten/src/test/java/org/apache/cloudstack/network/tungsten/service/TungstenElementTest.java +++ b/plugins/network-elements/tungsten/src/test/java/org/apache/cloudstack/network/tungsten/service/TungstenElementTest.java @@ -16,6 +16,7 @@ // under the License. package org.apache.cloudstack.network.tungsten.service; +import org.apache.cloudstack.acl.ApiKeyPairVO; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -360,6 +361,10 @@ public void applyLBRulesAddRuleSuccessTest() { when(lbStickinessPolicy.getMethodName()).thenReturn("AppCookie"); List> pairList = List.of(new Pair<>("cookieName", "cookieValue")); + ApiKeyPairVO latest = new ApiKeyPairVO(); + latest.setApiKey("apikey"); + latest.setSecretKey("secretkey"); + when(ApiDBUtils.searchForLatestUserKeyPair(Mockito.anyLong())).thenReturn(latest); when(lbStickinessPolicy.getParams()).thenReturn(pairList); when(loadBalancingRule1.getId()).thenReturn(1L); when(loadBalancingRule1.getState()).thenReturn(FirewallRule.State.Add); diff --git a/plugins/storage/image/s3/src/main/java/org/apache/cloudstack/storage/datastore/driver/S3ImageStoreDriverImpl.java b/plugins/storage/image/s3/src/main/java/org/apache/cloudstack/storage/datastore/driver/S3ImageStoreDriverImpl.java index 9b2f3ddd1001..6318c1920a99 100644 --- a/plugins/storage/image/s3/src/main/java/org/apache/cloudstack/storage/datastore/driver/S3ImageStoreDriverImpl.java +++ b/plugins/storage/image/s3/src/main/java/org/apache/cloudstack/storage/datastore/driver/S3ImageStoreDriverImpl.java @@ -55,7 +55,7 @@ public DataStoreTO getStoreTO(DataStore store) { return new S3TO(imgStore.getId(), imgStore.getUuid(), details.get(ApiConstants.S3_ACCESS_KEY), - details.get(ApiConstants.S3_SECRET_KEY), + details.get(ApiConstants.SECRET_KEY), details.get(ApiConstants.S3_END_POINT), details.get(ApiConstants.S3_BUCKET_NAME), details.get(ApiConstants.S3_SIGNER), diff --git a/server/src/main/java/com/cloud/api/ApiDBUtils.java b/server/src/main/java/com/cloud/api/ApiDBUtils.java index 4783815a9e56..342356f72cb4 100644 --- a/server/src/main/java/com/cloud/api/ApiDBUtils.java +++ b/server/src/main/java/com/cloud/api/ApiDBUtils.java @@ -321,7 +321,6 @@ import com.cloud.template.VirtualMachineTemplate; import com.cloud.user.Account; import com.cloud.user.AccountDetailsDao; -import com.cloud.user.AccountManager; import com.cloud.user.AccountService; import com.cloud.user.AccountVO; import com.cloud.user.ResourceLimitService; @@ -361,6 +360,8 @@ import com.cloud.vm.dao.VMInstanceDao; import com.cloud.vm.snapshot.VMSnapshot; import com.cloud.vm.snapshot.dao.VMSnapshotDao; +import org.apache.cloudstack.acl.ApiKeyPairVO; +import org.apache.cloudstack.acl.dao.ApiKeyPairDao; public class ApiDBUtils { private static ManagementServer s_ms; @@ -493,6 +494,7 @@ public class ApiDBUtils { static BackupOfferingDao s_backupOfferingDao; static NicDao s_nicDao; static ResourceManagerUtil s_resourceManagerUtil; + static ApiKeyPairDao s_apiKeyPairDao; static SnapshotPolicyDetailsDao s_snapshotPolicyDetailsDao; static ObjectStoreDao s_objectStoreDao; @@ -757,6 +759,8 @@ public class ApiDBUtils { @Inject private ResourceManagerUtil resourceManagerUtil; @Inject + private ApiKeyPairDao apiKeyPairDao; + @Inject SnapshotPolicyDetailsDao snapshotPolicyDetailsDao; @Inject @@ -899,6 +903,7 @@ void init() { s_backupOfferingDao = backupOfferingDao; s_resourceIconDao = resourceIconDao; s_resourceManagerUtil = resourceManagerUtil; + s_apiKeyPairDao = apiKeyPairDao; s_objectStoreDao = objectStoreDao; s_bucketDao = bucketDao; s_virtualMachineManager = virtualMachineManager; @@ -1949,10 +1954,8 @@ public static UserResponse newUserResponse(UserAccountJoinVO 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); - } + ApiKeyPairVO lastKeyPair = searchForLatestUserKeyPair(usr.getId()); + UserResponse response = s_userAccountJoinDao.newUserResponse(view, usr, lastKeyPair); // Populate user account role information if (usr.getAccountRoleId() != null) { Role role = s_roleService.findRole( usr.getAccountRoleId()); @@ -1969,6 +1972,10 @@ public static UserResponse newUserResponse(ResponseView view, Long domainId, Use return response; } + public static ApiKeyPairVO searchForLatestUserKeyPair(Long userId) { + return s_apiKeyPairDao.getLastApiKeyCreatedByUser(userId); + } + public static UserAccountJoinVO newUserView(User usr) { return s_userAccountJoinDao.newUserView(usr); } diff --git a/server/src/main/java/com/cloud/api/ApiResponseHelper.java b/server/src/main/java/com/cloud/api/ApiResponseHelper.java index 652480259c94..1a400e04310d 100644 --- a/server/src/main/java/com/cloud/api/ApiResponseHelper.java +++ b/server/src/main/java/com/cloud/api/ApiResponseHelper.java @@ -49,8 +49,16 @@ import com.cloud.dc.dao.VlanDetailsDao; import com.cloud.hypervisor.Hypervisor; import com.cloud.storage.BucketVO; +import com.cloud.domain.dao.DomainDao; +import com.cloud.user.AccountVO; +import com.cloud.user.ApiKeyPairState; +import com.cloud.user.dao.AccountDao; import org.apache.cloudstack.acl.ControlledEntity; import org.apache.cloudstack.acl.ControlledEntity.ACLType; +import org.apache.cloudstack.acl.RoleVO; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPair; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPairPermission; +import org.apache.cloudstack.acl.dao.RoleDao; import org.apache.cloudstack.affinity.AffinityGroup; import org.apache.cloudstack.affinity.AffinityGroupResponse; import org.apache.cloudstack.annotation.AnnotationService; @@ -77,6 +85,7 @@ import org.apache.cloudstack.api.response.BackupRepositoryResponse; import org.apache.cloudstack.api.response.BackupResponse; import org.apache.cloudstack.api.response.BackupScheduleResponse; +import org.apache.cloudstack.api.response.BaseRolePermissionResponse; import org.apache.cloudstack.api.response.BgpPeerResponse; import org.apache.cloudstack.api.response.BucketResponse; import org.apache.cloudstack.api.response.CapabilityResponse; @@ -122,6 +131,7 @@ import org.apache.cloudstack.api.response.Ipv4RouteResponse; import org.apache.cloudstack.api.response.Ipv6RouteResponse; import org.apache.cloudstack.api.response.IsolationMethodResponse; +import org.apache.cloudstack.api.response.ApiKeyPairResponse; import org.apache.cloudstack.api.response.LBHealthCheckPolicyResponse; import org.apache.cloudstack.api.response.LBHealthCheckResponse; import org.apache.cloudstack.api.response.LBStickinessPolicyResponse; @@ -522,6 +532,15 @@ public class ApiResponseHelper implements ResponseGenerator { @Inject RoutedIpv4Manager routedIpv4Manager; + @Inject + private RoleDao roleDao; + + @Inject + private AccountDao accountDao; + + @Inject + private DomainDao domainDao; + @Override public UserResponse createUserResponse(User user) { UserAccountJoinVO vUser = ApiDBUtils.newUserView(user); @@ -5482,4 +5501,79 @@ public SharedFSResponse createSharedFSResponse(ResponseView view, SharedFS share SharedFSJoinVO sharedFSView = ApiDBUtils.newSharedFSView(sharedFS); return ApiDBUtils.newSharedFSResponse(view, sharedFSView); } + + @Override + public ApiKeyPairResponse createKeyPairResponse(ApiKeyPair keyPair) { + ApiKeyPairResponse apiKeyPairResponse = new ApiKeyPairResponse(); + + populateApiKeyPairInApiKeyPairResponse(keyPair, apiKeyPairResponse); + populateUserInApiKeyPairResponse(keyPair, apiKeyPairResponse); + + AccountVO account = accountDao.findByIdIncludingRemoved(keyPair.getAccountId()); + apiKeyPairResponse.setAccountId(account.getUuid()); + apiKeyPairResponse.setAccountName(account.getAccountName()); + apiKeyPairResponse.setAccountType(account.getType().toString()); + + populateDomainInApiKeyPairResponse(account.getDomainId(), apiKeyPairResponse); + populateRoleInApiKeyPairResponse(account.getRoleId(), apiKeyPairResponse); + + return apiKeyPairResponse; + } + + protected void populateRoleInApiKeyPairResponse(Long roleId, ApiKeyPairResponse apiKeyPairResponse) { + RoleVO roleVO = roleDao.findById(roleId); + apiKeyPairResponse.setRoleId(roleVO.getUuid()); + apiKeyPairResponse.setRoleName(roleVO.getName()); + apiKeyPairResponse.setRoleType(roleVO.getRoleType().name()); + } + + protected static void populateApiKeyPairInApiKeyPairResponse(ApiKeyPair keyPair, ApiKeyPairResponse apiKeyPairResponse) { + apiKeyPairResponse.setName(keyPair.getName()); + apiKeyPairResponse.setApiKey(keyPair.getApiKey()); + apiKeyPairResponse.setSecretKey(keyPair.getSecretKey()); + apiKeyPairResponse.setDescription(keyPair.getDescription()); + apiKeyPairResponse.setId(keyPair.getUuid()); + apiKeyPairResponse.setCreated(keyPair.getCreated()); + apiKeyPairResponse.setStartDate(keyPair.getStartDate()); + apiKeyPairResponse.setEndDate(keyPair.getEndDate()); + + ApiKeyPairState state = ApiKeyPairState.ENABLED; + if (keyPair.getRemoved() != null) { + state = ApiKeyPairState.REMOVED; + } else if (keyPair.hasEndDatePassed()) { + state = ApiKeyPairState.EXPIRED; + } + apiKeyPairResponse.setState(state); + } + + protected void populateUserInApiKeyPairResponse(ApiKeyPair keyPair, ApiKeyPairResponse apiKeyPairResponse) { + User user = ApiDBUtils.findUserById(keyPair.getUserId()); + apiKeyPairResponse.setUserId(user.getUuid()); + apiKeyPairResponse.setUsername(user.getUsername()); + } + + protected void populateDomainInApiKeyPairResponse(Long domainId, ApiKeyPairResponse apiKeyPairResponse) { + DomainVO domainVO = domainDao.findById(domainId); + apiKeyPairResponse.setDomainId(domainVO.getUuid()); + apiKeyPairResponse.setDomainName(domainVO.getName()); + StringBuilder domainPath = new StringBuilder("ROOT"); + (domainPath.append(domainVO.getPath())).deleteCharAt(domainPath.length() - 1); + apiKeyPairResponse.setDomainPath(domainPath.toString()); + } + + @Override + public ListResponse createKeypairPermissionsResponse(final List permissions) { + final ListResponse response = new ListResponse<>(); + final List permissionResponses = new ArrayList<>(); + for (final ApiKeyPairPermission permission : permissions) { + BaseRolePermissionResponse permissionResponse = new BaseRolePermissionResponse(); + permissionResponse.setRule(permission.getRule()); + permissionResponse.setRulePermission(permission.getPermission()); + permissionResponse.setDescription(permission.getDescription()); + permissionResponse.setObjectName("keypermission"); + permissionResponses.add(permissionResponse); + } + response.setResponses(permissionResponses); + return response; + } } diff --git a/server/src/main/java/com/cloud/api/ApiServer.java b/server/src/main/java/com/cloud/api/ApiServer.java index b8227ef9d589..2b8056b10ee8 100644 --- a/server/src/main/java/com/cloud/api/ApiServer.java +++ b/server/src/main/java/com/cloud/api/ApiServer.java @@ -59,6 +59,7 @@ import com.cloud.cluster.ManagementServerHostVO; import com.cloud.cluster.dao.ManagementServerHostDao; +import com.cloud.utils.Ternary; import com.cloud.user.Account; import com.cloud.user.AccountManager; import com.cloud.user.AccountManagerImpl; @@ -67,6 +68,9 @@ import com.cloud.user.UserAccount; import com.cloud.user.UserVO; import org.apache.cloudstack.acl.APIChecker; +import org.apache.cloudstack.acl.ApiKeyPairManagerImpl; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPair; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPairPermission; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.ApiErrorCode; @@ -171,11 +175,11 @@ import com.cloud.exception.UnavailableCommandException; import com.cloud.projects.dao.ProjectDao; import com.cloud.storage.VolumeApiService; +import com.cloud.utils.HttpUtils.ApiSessionKeySameSite; +import com.cloud.utils.HttpUtils.ApiSessionKeyCheckOption; import com.cloud.utils.ConstantTimeComparator; import com.cloud.utils.DateUtil; import com.cloud.utils.HttpUtils; -import com.cloud.utils.HttpUtils.ApiSessionKeySameSite; -import com.cloud.utils.HttpUtils.ApiSessionKeyCheckOption; import com.cloud.utils.Pair; import com.cloud.utils.ReflectUtil; import com.cloud.utils.StringUtils; @@ -231,6 +235,8 @@ public class ApiServer extends ManagerBase implements HttpRequestHandler, ApiSer private UUIDManager uuidMgr; @Inject private UserPasswordResetManager userPasswordResetManager; + @Inject + private ApiKeyPairManagerImpl keyPairManager; private List pluggableServices; @@ -1031,14 +1037,16 @@ public boolean verifyRequest(final Map requestParameters, fina txn.close(); User user; // verify there is a user with this api key - final Pair userAcctPair = accountMgr.findUserByApiKey(apiKey); - if (userAcctPair == null) { + final Ternary keyPairTernary = accountMgr.findUserByApiKey(apiKey); + + if (keyPairTernary == null) { logger.debug("apiKey does not map to a valid user -- ignoring request, apiKey: {}", apiKey); return false; } - user = userAcctPair.first(); - final Account account = userAcctPair.second(); + user = keyPairTernary.first(); + Account account = keyPairTernary.second(); + ApiKeyPair keyPair = keyPairTernary.third(); if (user.getState() != Account.State.ENABLED || !account.getState().equals(Account.State.ENABLED)) { logger.info("disabled or locked user accessing the api, user = {} (state: {}); " + @@ -1054,10 +1062,16 @@ public boolean verifyRequest(final Map requestParameters, fina return false; } - // verify secret key exists - secretKey = user.getSecretKey(); + if (keyPair.getRemoved() != null) { + logger.info(String.format("Invalid request, as used API keypair [%s] has been removed.", keyPair.getUuid())); + return false; + } + + keyPair.validateDate(); + + secretKey = keyPair.getSecretKey(); if (secretKey == null) { - logger.info("User does not have a secret key associated with the account -- ignoring request, username: {}", user); + logger.info(String.format("User does not have a secret key associated with the API key -- ignoring request, username: [%s].", user.getUsername())); return false; } @@ -1075,21 +1089,29 @@ public boolean verifyRequest(final Map requestParameters, fina if (!equalSig) { signature = signature.replaceAll(SANITIZATION_REGEX, "_"); logger.info("User signature [{}] is not equaled to computed signature [{}].", signature, computedSignature); - } else { - CallContext.register(user, account); + return false; + } + CallContext.register(user, account); + + List keyPairPermissions = keyPairManager.findAllPermissionsByKeyPairId(keyPair.getId(), account.getRoleId()); + + if (commandAvailable(remoteAddress, commandName, user, keyPairPermissions.toArray(new ApiKeyPairPermission[0]))) { + logger.info(String.format("API accessed through API KeyPair. API Key: [%s]", keyPair.getApiKey())); + return true; } - return equalSig; } catch (final ServerApiException ex) { throw ex; + } catch (PermissionDeniedException ex) { + logger.error(String.format("Permission denied for keypair, reason: %s", ex.getMessage())); } catch (final Exception ex) { - logger.error("unable to verify request signature"); + logger.error("Unable to verify request signature.", ex); } return false; } - private boolean commandAvailable(final InetAddress remoteAddress, final String commandName, final User user) { + private boolean commandAvailable(final InetAddress remoteAddress, final String commandName, final User user, ApiKeyPairPermission... rolePermissions) { try { - checkCommandAvailable(user, commandName, remoteAddress); + checkCommandAvailable(user, commandName, remoteAddress, rolePermissions); } catch (final RequestLimitException ex) { logger.debug(ex.getMessage()); throw new ServerApiException(ApiErrorCode.API_LIMIT_EXCEED, ex.getMessage()); @@ -1353,7 +1375,7 @@ public boolean resetPassword(UserAccount userAccount, String token, String passw return userPasswordResetManager.validateAndResetPassword(userAccount, token, password); } - private void checkCommandAvailable(final User user, final String commandName, final InetAddress remoteAddress) throws PermissionDeniedException { + private void checkCommandAvailable(final User user, final String commandName, final InetAddress remoteAddress, ApiKeyPairPermission ... apiKeyPairPermissions) throws PermissionDeniedException { if (user == null) { throw new PermissionDeniedException("User is null for role based API access check for command" + commandName); } @@ -1367,12 +1389,11 @@ private void checkCommandAvailable(final User user, final String commandName, fi if (!NetUtils.isIpInCidrList(remoteAddress, accessAllowedCidrs.split(","))) { logger.warn("Request by account '" + account.toString() + "' was denied since " + remoteAddress + " does not match " + accessAllowedCidrs); throw new OriginDeniedException("Calls from disallowed origin", account, remoteAddress); - } + } } - for (final APIChecker apiChecker : apiAccessCheckers) { - apiChecker.checkAccess(user, commandName); + apiChecker.checkAccess(user, commandName, apiKeyPairPermissions); } } diff --git a/server/src/main/java/com/cloud/api/ApiServlet.java b/server/src/main/java/com/cloud/api/ApiServlet.java index 4994c42bb4dc..0c26bebf7293 100644 --- a/server/src/main/java/com/cloud/api/ApiServlet.java +++ b/server/src/main/java/com/cloud/api/ApiServlet.java @@ -295,7 +295,7 @@ void processRequestInContext(final HttpServletRequest req, final HttpServletResp return; } } else { - LOGGER.trace("no command available"); + LOGGER.trace("No command available."); } auditTrailSb.append(cleanQueryString); final boolean isNew = ((session == null) ? true : session.isNew()); @@ -305,7 +305,7 @@ void processRequestInContext(final HttpServletRequest req, final HttpServletResp // if a API key exists if (isNew && LOGGER.isTraceEnabled()) { - LOGGER.trace(String.format("new session: %s", session)); + LOGGER.trace(String.format("New session: %s.", session)); } if (!isNew && (command.equalsIgnoreCase(ValidateUserTwoFactorAuthenticationCodeCmd.APINAME) || (!skip2FAcheckForAPIs(command) && !skip2FAcheckForUser(session)))) { 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 5491bad0bb91..08c93d6cce38 100644 --- a/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java +++ b/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java @@ -36,6 +36,13 @@ import javax.inject.Inject; +import com.cloud.network.PublicIpQuarantine; +import com.cloud.network.dao.PublicIpQuarantineDao; +import com.cloud.network.vo.PublicIpQuarantineVO; +import org.apache.cloudstack.acl.RoleService; +import org.apache.cloudstack.acl.RoleVO; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPairService; +import org.apache.cloudstack.acl.dao.RoleDao; import com.cloud.dc.Pod; import com.cloud.dc.dao.DataCenterDao; import com.cloud.dc.dao.HostPodDao; @@ -45,6 +52,7 @@ import org.apache.cloudstack.acl.ControlledEntity; import org.apache.cloudstack.acl.ControlledEntity.ACLType; import org.apache.cloudstack.acl.SecurityChecker; +import org.apache.cloudstack.acl.dao.ApiKeyPairDao; import org.apache.cloudstack.affinity.AffinityGroupDomainMapVO; import org.apache.cloudstack.affinity.AffinityGroupResponse; import org.apache.cloudstack.affinity.AffinityGroupVMMapVO; @@ -255,7 +263,6 @@ import com.cloud.host.dao.HostTagsDao; import com.cloud.hypervisor.Hypervisor; import com.cloud.hypervisor.Hypervisor.HypervisorType; -import com.cloud.network.PublicIpQuarantine; import com.cloud.network.RouterHealthCheckResult; import com.cloud.network.VNF; import com.cloud.network.VpcVirtualNetworkApplianceService; @@ -266,13 +273,11 @@ import com.cloud.network.dao.IPAddressVO; import com.cloud.network.dao.NetworkDao; import com.cloud.network.dao.NetworkVO; -import com.cloud.network.dao.PublicIpQuarantineDao; import com.cloud.network.dao.RouterHealthCheckResultDao; import com.cloud.network.dao.RouterHealthCheckResultVO; import com.cloud.network.router.VirtualNetworkApplianceManager; import com.cloud.network.security.SecurityGroupVMMapVO; import com.cloud.network.security.dao.SecurityGroupVMMapDao; -import com.cloud.network.vo.PublicIpQuarantineVO; import com.cloud.offering.DiskOffering; import com.cloud.offering.ServiceOffering; import com.cloud.org.Grouping; @@ -371,6 +376,9 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q @Inject AccountManager accountMgr; + @Inject + RoleService roleService; + @Inject ProjectManager _projectMgr; @@ -642,6 +650,15 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q @Inject HostPodDao podDao; + @Inject + ApiKeyPairDao apiKeyPairDao; + + @Inject + ApiKeyPairService apiKeyPairService; + + @Inject + RoleDao roleDao; + private SearchCriteria getMinimumCpuServiceOfferingJoinSearchCriteria(int cpu) { SearchCriteria sc = _srvOfferingJoinDao.createSearchCriteria(); SearchCriteria sc1 = _srvOfferingJoinDao.createSearchCriteria(); @@ -864,6 +881,20 @@ private Pair, Integer> getUserListInternal(Account calle return _userAccountJoinDao.searchAndCount(sc, searchFilter); } + @Override + public List searchForAccessableUsers() { + List permittedAccounts = new ArrayList<>(); + Account callingAccount = CallContext.current().getCallingAccount(); + Filter searchFilter = new Filter(UserAccountJoinVO.class, "id", true); + List allowedRoles = roleDao.listAll(); + roleService.removeRolesIfNeeded(allowedRoles); + List allowedRolesId = allowedRoles.stream().map(RoleVO::getId).collect(Collectors.toList()); + + Pair, Integer> usersPair = getUserListInternal(callingAccount, permittedAccounts, + true, null, null, null, null, null, null, null, callingAccount.getDomainId(), true, searchFilter, null); + return usersPair.first().stream().filter(userAccount -> allowedRolesId.contains(userAccount.getAccountRoleId())).map(UserAccountJoinVO::getId).collect(Collectors.toList()); + } + @Override public ListResponse searchForEvents(ListEventsCmd cmd) { Pair, Integer> result = searchForEventsInternal(cmd); diff --git a/server/src/main/java/com/cloud/api/query/dao/UserAccountJoinDao.java b/server/src/main/java/com/cloud/api/query/dao/UserAccountJoinDao.java index cff758d0c171..e0d88fbfde7c 100644 --- a/server/src/main/java/com/cloud/api/query/dao/UserAccountJoinDao.java +++ b/server/src/main/java/com/cloud/api/query/dao/UserAccountJoinDao.java @@ -19,6 +19,7 @@ import java.util.List; import org.apache.cloudstack.api.ResponseObject; +import org.apache.cloudstack.acl.ApiKeyPairVO; import org.apache.cloudstack.api.response.UserResponse; import com.cloud.api.query.vo.UserAccountJoinVO; @@ -28,7 +29,7 @@ public interface UserAccountJoinDao extends GenericDao { - UserResponse newUserResponse(ResponseObject.ResponseView responseView, UserAccountJoinVO usr); + UserResponse newUserResponse(ResponseObject.ResponseView responseView, UserAccountJoinVO usr, ApiKeyPairVO lastKeyPair); UserAccountJoinVO newUserView(User usr); diff --git a/server/src/main/java/com/cloud/api/query/dao/UserAccountJoinDaoImpl.java b/server/src/main/java/com/cloud/api/query/dao/UserAccountJoinDaoImpl.java index f2c234b4c7cb..f24305d53554 100644 --- a/server/src/main/java/com/cloud/api/query/dao/UserAccountJoinDaoImpl.java +++ b/server/src/main/java/com/cloud/api/query/dao/UserAccountJoinDaoImpl.java @@ -21,6 +21,7 @@ import com.cloud.user.AccountManagerImpl; import org.apache.cloudstack.api.ResponseObject.ResponseView; +import org.apache.cloudstack.acl.ApiKeyPairVO; import org.springframework.stereotype.Component; import org.apache.cloudstack.api.response.UserResponse; @@ -53,7 +54,7 @@ protected UserAccountJoinDaoImpl() { } @Override - public UserResponse newUserResponse(ResponseView view, UserAccountJoinVO usr) { + public UserResponse newUserResponse(ResponseView view, UserAccountJoinVO usr, ApiKeyPairVO lastKeyPair) { UserResponse userResponse = new UserResponse(); userResponse.setAccountId(usr.getAccountUuid()); userResponse.setAccountName(usr.getAccountName()); @@ -69,10 +70,12 @@ public UserResponse newUserResponse(ResponseView view, UserAccountJoinVO usr) { userResponse.setState(usr.getState().toString()); userResponse.setTimezone(usr.getTimezone()); userResponse.setUsername(usr.getUsername()); - userResponse.setApiKey(usr.getApiKey()); - userResponse.setSecretKey(usr.getSecretKey()); userResponse.setIsDefault(usr.isDefault()); userResponse.set2FAenabled(usr.isUser2faEnabled()); + if (lastKeyPair != null) { + userResponse.setApiKey(lastKeyPair.getApiKey()); + userResponse.setSecretKey(lastKeyPair.getSecretKey()); + } long domainId = usr.getDomainId(); boolean is2FAmandated = Boolean.TRUE.equals(AccountManagerImpl.enableUserTwoFactorAuthentication.valueIn(domainId)) && Boolean.TRUE.equals(AccountManagerImpl.mandateUserTwoFactorAuthentication.valueIn(domainId)); userResponse.set2FAmandated(is2FAmandated); diff --git a/server/src/main/java/com/cloud/api/query/vo/UserAccountJoinVO.java b/server/src/main/java/com/cloud/api/query/vo/UserAccountJoinVO.java index ad005eebb76e..774759da607b 100644 --- a/server/src/main/java/com/cloud/api/query/vo/UserAccountJoinVO.java +++ b/server/src/main/java/com/cloud/api/query/vo/UserAccountJoinVO.java @@ -19,7 +19,6 @@ import com.cloud.user.Account; import com.cloud.user.User; import com.cloud.user.UserAccount; -import com.cloud.utils.db.Encrypt; import com.cloud.utils.db.GenericDao; import org.apache.cloudstack.api.Identity; import org.apache.cloudstack.api.InternalIdentity; @@ -61,13 +60,6 @@ public class UserAccountJoinVO extends BaseViewVO implements InternalIdentity, I @Column(name = "state") private String state; - @Column(name = "api_key") - private String apiKey = null; - - @Encrypt - @Column(name = "secret_key") - private String secretKey = null; - @Column(name = GenericDao.CREATED_COLUMN) private Date created; @@ -209,14 +201,6 @@ public String getState() { return state; } - public String getApiKey() { - return apiKey; - } - - public String getSecretKey() { - return secretKey; - } - public Date getCreated() { return created; } diff --git a/server/src/main/java/com/cloud/network/as/AutoScaleManagerImpl.java b/server/src/main/java/com/cloud/network/as/AutoScaleManagerImpl.java index bda84f09fe61..9372fdb76634 100644 --- a/server/src/main/java/com/cloud/network/as/AutoScaleManagerImpl.java +++ b/server/src/main/java/com/cloud/network/as/AutoScaleManagerImpl.java @@ -39,6 +39,7 @@ import javax.inject.Inject; import com.cloud.network.NetworkModel; +import org.apache.cloudstack.acl.ApiKeyPairVO; import org.apache.cloudstack.acl.ControlledEntity; import org.apache.cloudstack.affinity.AffinityGroupVO; import org.apache.cloudstack.affinity.dao.AffinityGroupDao; @@ -510,16 +511,11 @@ public void checkAutoScaleUser(Long autoscaleUserId, long accountId) { throw new InvalidParameterValueException("AutoScale User id does not belong to the same account"); } - String apiKey = user.getApiKey(); - String secretKey = user.getSecretKey(); + ApiKeyPairVO latestKeypair = ApiDBUtils.searchForLatestUserKeyPair(user.getId()); String csUrl = ApiServiceConfiguration.ApiServletPath.value(); - if (apiKey == null) { - throw new InvalidParameterValueException("apiKey for user: " + user.getUsername() + " is empty. Please generate it"); - } - - if (secretKey == null) { - throw new InvalidParameterValueException("secretKey for user: " + user.getUsername() + " is empty. Please generate it"); + if (latestKeypair == null) { + throw new InvalidParameterValueException(String.format("No API keypair for user [%s]. Please generate it.", user.getUsername())); } if (csUrl == null || csUrl.contains("localhost")) { diff --git a/server/src/main/java/com/cloud/network/lb/LoadBalancingRulesManagerImpl.java b/server/src/main/java/com/cloud/network/lb/LoadBalancingRulesManagerImpl.java index 015cbe490494..d17fdc58ca1d 100644 --- a/server/src/main/java/com/cloud/network/lb/LoadBalancingRulesManagerImpl.java +++ b/server/src/main/java/com/cloud/network/lb/LoadBalancingRulesManagerImpl.java @@ -16,6 +16,7 @@ // under the License. package com.cloud.network.lb; +import com.cloud.api.ApiDBUtils; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -30,6 +31,7 @@ import com.cloud.offerings.NetworkOfferingServiceMapVO; import com.cloud.offerings.dao.NetworkOfferingServiceMapDao; +import org.apache.cloudstack.acl.ApiKeyPairVO; import org.apache.cloudstack.acl.SecurityChecker; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.ApiErrorCode; @@ -347,16 +349,16 @@ private LbAutoScaleVmGroup getLbAutoScaleVmGroup(AutoScaleVmGroupVO vmGroup, Aut if (user == null) { throw new InvalidParameterValueException("Unable to find user by id " + autoscaleUserId); } - apiKey = user.getApiKey(); - secretKey = user.getSecretKey(); - if (apiKey == null) { - throw new InvalidParameterValueException("apiKey for user: " + user.getUsername() + " is empty. Please generate it"); - } - if (secretKey == null) { - throw new InvalidParameterValueException("secretKey for user: " + user.getUsername() + " is empty. Please generate it"); + ApiKeyPairVO latestKeypair = ApiDBUtils.searchForLatestUserKeyPair(user.getId()); + + if (latestKeypair == null) { + throw new InvalidParameterValueException(String.format("No API keypair for user [%s]. Please generate it.", user.getUsername())); } + apiKey = latestKeypair.getApiKey(); + secretKey = latestKeypair.getSecretKey(); + if (csUrl == null || csUrl.contains("localhost")) { throw new InvalidParameterValueException(String.format("Global setting %s has to be set to the Management Server's API end point", ApiServiceConfiguration.ApiServletPath.key())); } diff --git a/server/src/main/java/com/cloud/network/router/VirtualNetworkApplianceManagerImpl.java b/server/src/main/java/com/cloud/network/router/VirtualNetworkApplianceManagerImpl.java index e171b68399bf..c7f6d4152537 100644 --- a/server/src/main/java/com/cloud/network/router/VirtualNetworkApplianceManagerImpl.java +++ b/server/src/main/java/com/cloud/network/router/VirtualNetworkApplianceManagerImpl.java @@ -17,6 +17,7 @@ package com.cloud.network.router; +import com.cloud.api.ApiDBUtils; import static com.cloud.utils.NumbersUtil.toHumanReadableSize; import java.lang.reflect.Type; @@ -48,6 +49,7 @@ import javax.inject.Inject; import javax.naming.ConfigurationException; +import org.apache.cloudstack.acl.ApiKeyPairVO; import org.apache.cloudstack.alert.AlertService; import org.apache.cloudstack.alert.AlertService.AlertType; import org.apache.cloudstack.api.ApiCommandResourceType; @@ -2107,8 +2109,14 @@ public boolean finalizeVirtualMachineProfile(final VirtualMachineProfile profile logger.warn(String .format("global setting[baremetal.provision.done.notification] is enabled but user baremetal-system-account is not found. Baremetal provision done notification will not be enabled")); } else { - buf.append(String.format(" baremetalnotificationsecuritykey=%s", user.getSecretKey())); - buf.append(String.format(" baremetalnotificationapikey=%s", user.getApiKey())); + ApiKeyPairVO latestKeypair = ApiDBUtils.searchForLatestUserKeyPair(user.getId()); + + if (latestKeypair == null) { + throw new InvalidParameterValueException(String.format("No API keypair for user [%s]. Please generate it.", user.getUsername())); + } + + buf.append(String.format(" baremetalnotificationsecuritykey=%s", latestKeypair.getSecretKey())); + buf.append(String.format(" baremetalnotificationapikey=%s", latestKeypair.getApiKey())); buf.append(" host=").append(ApiServiceConfiguration.ManagementServerAddresses.value()); buf.append(" port=").append(_configDao.getValue(Config.BaremetalProvisionDoneNotificationPort.key())); } diff --git a/server/src/main/java/com/cloud/server/ManagementServerImpl.java b/server/src/main/java/com/cloud/server/ManagementServerImpl.java index 9d734d4fd3b6..79ceda334c2b 100644 --- a/server/src/main/java/com/cloud/server/ManagementServerImpl.java +++ b/server/src/main/java/com/cloud/server/ManagementServerImpl.java @@ -44,8 +44,10 @@ import javax.inject.Inject; import javax.naming.ConfigurationException; +import org.apache.cloudstack.acl.ApiKeyPairVO; import org.apache.cloudstack.acl.ControlledEntity; import org.apache.cloudstack.acl.SecurityChecker; +import org.apache.cloudstack.acl.dao.ApiKeyPairDao; import org.apache.cloudstack.affinity.AffinityGroupProcessor; import org.apache.cloudstack.affinity.dao.AffinityGroupVMMapDao; import org.apache.cloudstack.annotation.AnnotationService; @@ -271,15 +273,18 @@ import org.apache.cloudstack.api.command.admin.usage.RemoveRawUsageRecordsCmd; import org.apache.cloudstack.api.command.admin.usage.UpdateTrafficTypeCmd; import org.apache.cloudstack.api.command.admin.user.CreateUserCmd; +import org.apache.cloudstack.api.command.admin.user.DeleteUserKeysCmd; import org.apache.cloudstack.api.command.admin.user.DeleteUserCmd; import org.apache.cloudstack.api.command.admin.user.DisableUserCmd; import org.apache.cloudstack.api.command.admin.user.EnableUserCmd; import org.apache.cloudstack.api.command.admin.user.GetUserCmd; import org.apache.cloudstack.api.command.admin.user.GetUserKeysCmd; +import org.apache.cloudstack.api.command.admin.user.ListUserKeyRulesCmd; +import org.apache.cloudstack.api.command.admin.user.ListUserKeysCmd; import org.apache.cloudstack.api.command.admin.user.ListUsersCmd; import org.apache.cloudstack.api.command.admin.user.LockUserCmd; import org.apache.cloudstack.api.command.admin.user.MoveUserCmd; -import org.apache.cloudstack.api.command.admin.user.RegisterCmd; +import org.apache.cloudstack.api.command.admin.user.RegisterUserKeysCmd; import org.apache.cloudstack.api.command.admin.user.UpdateUserCmd; import org.apache.cloudstack.api.command.admin.vlan.CreateVlanIpRangeCmd; import org.apache.cloudstack.api.command.admin.vlan.DedicatePublicIpRangeCmd; @@ -943,6 +948,8 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe @Inject protected SSHKeyPairDao _sshKeyPairDao; @Inject + protected ApiKeyPairDao _apiKeyPairDao; + @Inject private LoadBalancerDao _loadbalancerDao; @Inject private HypervisorCapabilitiesDao _hypervisorCapabilitiesDao; @@ -3656,7 +3663,7 @@ public List> getCommands() { cmdList.add(ListUsersCmd.class); cmdList.add(LockUserCmd.class); cmdList.add(MoveUserCmd.class); - cmdList.add(RegisterCmd.class); + cmdList.add(RegisterUserKeysCmd.class); cmdList.add(UpdateUserCmd.class); cmdList.add(CreateVlanIpRangeCmd.class); cmdList.add(UpdateVlanIpRangeCmd.class); @@ -4069,6 +4076,9 @@ public List> getCommands() { cmdList.add(ChangeOutOfBandManagementPasswordCmd.class); cmdList.add(GetUserKeysCmd.class); cmdList.add(CreateConsoleEndpointCmd.class); + cmdList.add(DeleteUserKeysCmd.class); + cmdList.add(ListUserKeysCmd.class); + cmdList.add(ListUserKeyRulesCmd.class); //user data APIs cmdList.add(RegisterUserDataCmd.class); @@ -4468,7 +4478,13 @@ public ArrayList getCloudIdentifierResponse(final long userId) { try { // get the user obj to get their secret key user = _accountMgr.getActiveUser(userId); - final String secretKey = user.getSecretKey(); + ApiKeyPairVO latestKeypair = ApiDBUtils.searchForLatestUserKeyPair(user.getId()); + + if (latestKeypair == null) { + throw new InvalidParameterValueException(String.format("No API keypair for user [%s]. Please generate it.", user.getUsername())); + } + + final String secretKey = latestKeypair.getSecretKey(); final String input = cloudIdentifier; signature = signRequest(input, secretKey); } catch (final Exception e) { diff --git a/server/src/main/java/com/cloud/servlet/ConsoleProxyServlet.java b/server/src/main/java/com/cloud/servlet/ConsoleProxyServlet.java index 2b786a8f1efe..bb8f2b2e4709 100644 --- a/server/src/main/java/com/cloud/servlet/ConsoleProxyServlet.java +++ b/server/src/main/java/com/cloud/servlet/ConsoleProxyServlet.java @@ -35,6 +35,7 @@ import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPair; import org.apache.cloudstack.framework.security.keys.KeysManager; import org.apache.commons.codec.binary.Base64; import org.apache.logging.log4j.Logger; @@ -538,24 +539,23 @@ private boolean verifyRequest(Map requestParameters) { txn.close(); User user = null; // verify there is a user with this api key - Pair userAcctPair = _accountMgr.findUserByApiKey(apiKey); - if (userAcctPair == null) { + Ternary keyPairTernary = _accountMgr.findUserByApiKey(apiKey); + if (keyPairTernary == null) { LOGGER.debug("apiKey does not map to a valid user -- ignoring request, apiKey: " + apiKey); return false; } - user = userAcctPair.first(); - Account account = userAcctPair.second(); + user = keyPairTernary.first(); + Account account = keyPairTernary.second(); + ApiKeyPair keyPair = keyPairTernary.third(); if (!user.getState().equals(Account.State.ENABLED) || !account.getState().equals(Account.State.ENABLED)) { LOGGER.debug("disabled or locked user accessing the api, user: {}; state: {}; accountState: {}", user, user.getState(), account.getState()); return false; } - // verify secret key exists - secretKey = user.getSecretKey(); - if (secretKey == null) { - LOGGER.debug("User does not have a secret key associated with the account -- ignoring request, user: {}", user); + if (keyPair == null) { + LOGGER.debug("User does not have a keypair associated with the account -- ignoring request, username: " + user.getUsername()); return false; } diff --git a/server/src/main/java/com/cloud/user/AccountManager.java b/server/src/main/java/com/cloud/user/AccountManager.java index c46ac78526b6..f3f6a6cd7a8a 100644 --- a/server/src/main/java/com/cloud/user/AccountManager.java +++ b/server/src/main/java/com/cloud/user/AccountManager.java @@ -21,6 +21,7 @@ import java.util.Map; import org.apache.cloudstack.acl.ControlledEntity; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPair; import org.apache.cloudstack.api.command.admin.account.UpdateAccountCmd; import org.apache.cloudstack.api.command.admin.user.DeleteUserCmd; import org.apache.cloudstack.api.command.admin.user.MoveUserCmd; @@ -35,7 +36,6 @@ import com.cloud.exception.ConcurrentOperationException; import com.cloud.exception.ResourceUnavailableException; import com.cloud.projects.Project.ListProjectResourcesCriteria; -import com.cloud.utils.Pair; import com.cloud.utils.Ternary; import com.cloud.utils.db.SearchBuilder; import com.cloud.utils.db.SearchCriteria; @@ -85,7 +85,7 @@ public interface AccountManager extends AccountService, Configurable { * that was created for a particular user * @return the user/account pair if one exact match was found, null otherwise */ - Pair findUserByApiKey(String apiKey); + Ternary findUserByApiKey(String apiKey); boolean enableAccount(long accountId); diff --git a/server/src/main/java/com/cloud/user/AccountManagerImpl.java b/server/src/main/java/com/cloud/user/AccountManagerImpl.java index 19eba061e131..e57fbf646b8e 100644 --- a/server/src/main/java/com/cloud/user/AccountManagerImpl.java +++ b/server/src/main/java/com/cloud/user/AccountManagerImpl.java @@ -23,11 +23,13 @@ import java.util.Arrays; import java.util.Collections; import java.util.Comparator; +import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.UUID; import java.util.concurrent.Executors; @@ -42,27 +44,48 @@ import javax.inject.Inject; import javax.naming.ConfigurationException; +import com.cloud.user.dao.AccountDao; +import com.cloud.user.dao.SSHKeyPairDao; +import com.cloud.user.dao.UserAccountDao; +import com.cloud.user.dao.UserDao; import org.apache.cloudstack.acl.APIChecker; +import org.apache.cloudstack.acl.ApiKeyPairManagerImpl; +import org.apache.cloudstack.acl.ApiKeyPairPermissionVO; +import org.apache.cloudstack.acl.ApiKeyPairVO; import org.apache.cloudstack.acl.ControlledEntity; import org.apache.cloudstack.acl.InfrastructureEntity; import org.apache.cloudstack.acl.QuerySelector; import org.apache.cloudstack.acl.Role; +import org.apache.cloudstack.acl.RolePermission; +import org.apache.cloudstack.acl.RolePermissionEntity; import org.apache.cloudstack.acl.RoleService; import org.apache.cloudstack.acl.RoleType; import org.apache.cloudstack.acl.SecurityChecker; import org.apache.cloudstack.acl.SecurityChecker.AccessType; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPair; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPairPermission; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPairService; +import org.apache.cloudstack.acl.dao.ApiKeyPairDao; +import org.apache.cloudstack.acl.dao.ApiKeyPairPermissionsDao; import org.apache.cloudstack.affinity.AffinityGroup; import org.apache.cloudstack.affinity.dao.AffinityGroupDao; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiCommandResourceType; import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseAsyncCmd; import org.apache.cloudstack.api.command.admin.account.CreateAccountCmd; import org.apache.cloudstack.api.command.admin.account.UpdateAccountCmd; import org.apache.cloudstack.api.command.admin.user.DeleteUserCmd; +import org.apache.cloudstack.api.command.admin.user.DeleteUserKeysCmd; import org.apache.cloudstack.api.command.admin.user.GetUserKeysCmd; +import org.apache.cloudstack.api.command.admin.user.ListUserKeyRulesCmd; +import org.apache.cloudstack.api.command.admin.user.ListUserKeysCmd; import org.apache.cloudstack.api.command.admin.user.MoveUserCmd; -import org.apache.cloudstack.api.command.admin.user.RegisterCmd; +import org.apache.cloudstack.api.command.admin.user.RegisterUserKeysCmd; import org.apache.cloudstack.api.command.admin.user.UpdateUserCmd; +import org.apache.cloudstack.api.response.ApiKeyPairResponse; +import org.apache.cloudstack.api.response.BaseRolePermissionResponse; +import org.apache.cloudstack.api.response.ListResponse; import org.apache.cloudstack.api.response.UserTwoFactorAuthenticationSetupResponse; import org.apache.cloudstack.auth.UserAuthenticator; import org.apache.cloudstack.auth.UserAuthenticator.ActionOnFailedAuthentication; @@ -75,6 +98,7 @@ import org.apache.cloudstack.framework.messagebus.MessageBus; import org.apache.cloudstack.framework.messagebus.PublishScope; import org.apache.cloudstack.managed.context.ManagedContextRunnable; +import org.apache.cloudstack.query.QueryService; import org.apache.cloudstack.network.RoutedIpv4Manager; import org.apache.cloudstack.network.dao.NetworkPermissionDao; import org.apache.cloudstack.region.gslb.GlobalLoadBalancerRuleDao; @@ -85,6 +109,7 @@ import org.apache.commons.codec.binary.Base64; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang3.BooleanUtils; +import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.jetbrains.annotations.NotNull; import org.springframework.beans.factory.NoSuchBeanDefinitionException; @@ -167,10 +192,6 @@ import com.cloud.template.TemplateManager; import com.cloud.template.VirtualMachineTemplate; import com.cloud.user.Account.State; -import com.cloud.user.dao.AccountDao; -import com.cloud.user.dao.SSHKeyPairDao; -import com.cloud.user.dao.UserAccountDao; -import com.cloud.user.dao.UserDao; import com.cloud.user.dao.UserDataDao; import com.cloud.utils.ConstantTimeComparator; import com.cloud.utils.NumbersUtil; @@ -207,6 +228,8 @@ import com.cloud.vm.snapshot.VMSnapshotManager; import com.cloud.vm.snapshot.VMSnapshotVO; import com.cloud.vm.snapshot.dao.VMSnapshotDao; +import org.apache.cloudstack.api.BaseCmd; +import org.joda.time.DateTime; public class AccountManagerImpl extends ManagerBase implements AccountManager, Manager { @@ -223,6 +246,14 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M @Inject private InstanceGroupDao _vmGroupDao; @Inject + private ApiKeyPairDao apiKeyPairDao; + @Inject + private ApiKeyPairService apiKeyPairService; + @Inject + private ApiKeyPairPermissionsDao apiKeyPairPermissionsDao; + @Inject + private ApiKeyPairManagerImpl keyPairManager; + @Inject private UserAccountDao _userAccountDao; @Inject private VolumeDao _volumeDao; @@ -289,6 +320,8 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M @Inject private VolumeApiService volumeService; @Inject + private QueryService queryService; + @Inject private AffinityGroupDao _affinityGroupDao; @Inject private AccountGuestVlanMapDao _accountGuestVlanMapDao; @@ -391,6 +424,8 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M true, ConfigKey.Scope.Domain); + private Map> annotationRoleBasedApisMap = new HashMap<>(); + static ConfigKey userAllowMultipleAccounts = new ConfigKey<>("Advanced", Boolean.class, "user.allow.multiple.accounts", @@ -417,6 +452,9 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M protected AccountManagerImpl() { super(); + for (RoleType roleType : RoleType.values()) { + annotationRoleBasedApisMap.put(roleType, new HashSet<>()); + } } public List getUserAuthenticators() { @@ -905,6 +943,11 @@ public boolean deleteAccount(AccountVO account, long callerUserId, Account calle return cleanupAccount(account, callerUserId, caller); } + protected void removeUserApiKeys(Long userId) { + List apiKeyPairs = apiKeyPairDao.listApiKeysByUserOrApiKeyId(userId, null).first(); + apiKeyPairs.forEach(keyPair -> _accountService.deleteApiKey(keyPair)); + } + protected void cleanupPluginsResourcesIfNeeded(Account account) { try { KubernetesServiceHelper kubernetesServiceHelper = @@ -923,6 +966,7 @@ protected boolean cleanupAccount(AccountVO account, long callerUserId, Account c // cleanup the users from the account List users = _userDao.listByAccount(accountId); for (UserVO user : users) { + removeUserApiKeys(user.getId()); if (!_userDao.remove(user.getId())) { logger.error("Unable to delete user: " + user + " as a part of account " + account + " cleanup"); accountCleanupNeeded = true; @@ -1551,7 +1595,7 @@ public UserAccount updateUser(UpdateUserCmd updateUserCmd) { logger.debug("Updating user {}", user); - validateAndUpdateApiAndSecretKeyIfNeeded(updateUserCmd, user); + ApiKeyPairVO keyPair = validateAndUpdateApiAndSecretKeyIfNeeded(updateUserCmd, user); validateAndUpdateUserApiKeyAccess(updateUserCmd, user); validateAndUpdateFirstNameIfNeeded(updateUserCmd, user); @@ -1571,6 +1615,9 @@ public UserAccount updateUser(UpdateUserCmd updateUserCmd) { if (mandate2FA != null && mandate2FA) { user.setUser2faEnabled(true); } + if (keyPair != null) { + apiKeyPairDao.update(keyPair.getId(), keyPair); + } _userDao.update(user.getId(), user); return _userAccountDao.findById(user.getId()); } @@ -1838,7 +1885,7 @@ protected Account getCurrentCallingAccount() { *
  • If a pair of keys is provided, we validate to see if there is an user already using the provided API key. If there is someone else using, we throw an {@link InvalidParameterValueException} because two users cannot have the same API key. * */ - protected void validateAndUpdateApiAndSecretKeyIfNeeded(UpdateUserCmd updateUserCmd, UserVO user) { + protected ApiKeyPairVO validateAndUpdateApiAndSecretKeyIfNeeded(UpdateUserCmd updateUserCmd, UserVO user) { String apiKey = updateUserCmd.getApiKey(); String secretKey = updateUserCmd.getSecretKey(); @@ -1848,17 +1895,28 @@ protected void validateAndUpdateApiAndSecretKeyIfNeeded(UpdateUserCmd updateUser throw new InvalidParameterValueException("Please provide a userApiKey/userSecretKey pair"); } if (isApiKeyBlank && isSecretKeyBlank) { - return; + return null; } - Pair apiKeyOwner = _accountDao.findUserAccountByApiKey(apiKey); - if (apiKeyOwner != null) { - User userThatHasTheProvidedApiKey = apiKeyOwner.first(); - if (userThatHasTheProvidedApiKey.getId() != user.getId()) { - throw new InvalidParameterValueException(String.format("The API key [%s] already exists in the system. Please provide a unique key.", apiKey)); - } + Ternary keyPairTernary = _accountDao.findUserAccountByApiKey(apiKey); + if (keyPairTernary == null) { + throw new InvalidParameterValueException(String.format("The API key [%s] does not exist in the system. Please provide a valid key.", apiKey)); } - user.setApiKey(apiKey); - user.setSecretKey(secretKey); + + User userThatHasTheProvidedApiKey = keyPairTernary.first(); + if (userThatHasTheProvidedApiKey.getId() != user.getId()) { + throw new InvalidParameterValueException(String.format("The API key [%s] already exists in the system. Please provide a unique key.", apiKey)); + } + + ApiKeyPairVO keyPair = (ApiKeyPairVO) keyPairTernary.third(); + + Account account = _accountDao.findById(user.getAccountId()); + keyPair.setApiKey(apiKey); + keyPair.setSecretKey(secretKey); + keyPair.setDomainId(account.getDomainId()); + keyPair.setUserId(user.getId()); + keyPair.setAccountId(account.getId()); + keyPair.setName(keyPair.getUuid()); + return keyPair; } protected void validateAndUpdateUserApiKeyAccess(UpdateUserCmd updateUserCmd, UserVO user) { @@ -2359,6 +2417,9 @@ public boolean deleteUser(DeleteUserCmd deleteUserCmd) { // don't allow to delete the user from the account of type Project checkAccountAndAccess(user, account); verifyCallerPrivilegeForUserOrAccountOperations(user); + + removeUserApiKeys(id); + return _userDao.remove(id); } @@ -2398,8 +2459,6 @@ public Boolean doInTransaction(TransactionStatus status) { UserVO newUser = new UserVO(user); user.setExternalEntity(user.getUuid()); user.setUuid(UUID.randomUUID().toString()); - user.setApiKey(null); - user.setSecretKey(null); _userDao.update(user.getId(), user); newUser.setAccountId(newAccountId); boolean success = _userDao.remove(user.getId()); @@ -3037,30 +3096,42 @@ protected void updateLoginAttemptsWhenIncorrectLoginAttemptsEnabled(UserAccount } @Override - public Pair findUserByApiKey(String apiKey) { + public Ternary findUserByApiKey(String apiKey) { return _accountDao.findUserAccountByApiKey(apiKey); } @Override public Pair> getKeys(GetUserKeysCmd cmd) { - final long userId = cmd.getID(); - return getKeys(userId); - } - - @Override - public Pair> getKeys(Long userId) { + final long userId = cmd.getId(); User user = getActiveUser(userId); if (user == null) { - throw new InvalidParameterValueException("Unable to find user by id"); + throw new InvalidParameterValueException(String.format("Unable to find active user with ID [%s].", userId)); } - final Account account = getAccount(getUserAccountById(userId).getAccountId()); //Extracting the Account from the userID of the requested user. + final Account account = getAccount(user.getAccountId()); User caller = CallContext.current().getCallingUser(); checkAccess(caller, account); verifyCallerPrivilegeForUserOrAccountOperations(user); + String accessingApiKey = getAccessingApiKey(cmd); + + ApiKeyPair keyPair; + if (accessingApiKey != null) { + ApiKeyPair accessingKeyPair = apiKeyPairService.findByApiKey(accessingApiKey); + if (userId == accessingKeyPair.getUserId()) { + keyPair = apiKeyPairService.findByApiKey(accessingApiKey); + } else { + keyPair = _accountService.getLatestUserKeyPair(userId); + } + } else { + keyPair = _accountService.getLatestUserKeyPair(userId); + } + + validateKeyPairIsNotNull(keyPair); + validateAccessingKeyPairPermissionsIsSupersetOfAccessedKeyPair(keyPair, cmd); + Map keys = new HashMap<>(); - keys.put("apikey", user.getApiKey()); - keys.put("secretkey", user.getSecretKey()); + keys.put("apikey", keyPair.getApiKey()); + keys.put("secretkey", keyPair.getSecretKey()); Boolean apiKeyAccess = user.getApiKeyAccess(); if (apiKeyAccess == null) { @@ -3073,6 +3144,207 @@ public Pair> getKeys(Long userId) { return new Pair<>(apiKeyAccess, keys); } + @Override + public ListResponse listKeys(ListUserKeysCmd cmd) { + ListResponse finalResponse = new ListResponse<>(); + List responses = new ArrayList<>(); + + if (cmd.getKeyId() != null || cmd.getApiKeyFilter() != null) { + fetchOnlyOneKeypair(responses, cmd); + finalResponse.setResponses(responses); + return finalResponse; + } + + Integer total = fetchMultipleKeypairs(responses, cmd); + finalResponse.setResponses(responses, total); + return finalResponse; + } + + private void fetchOnlyOneKeypair(List responses, ListUserKeysCmd cmd) { + ApiKeyPair keyPair; + if (cmd.getKeyId() != null) { + keyPair = _accountService.getKeyPairById(cmd.getKeyId()); + } else { + keyPair = _accountService.getKeyPairByApiKey(cmd.getApiKeyFilter()); + } + + validateKeyPairIsNotNull(keyPair); + validateAccessingKeyPairPermissionsIsSupersetOfAccessedKeyPair(keyPair, cmd); + + _accountService.validateCallingUserHasAccessToDesiredUser(keyPair.getUserId()); + markExpiredKeysWithStateExpired(keyPair); + + addKeypairResponse(keyPair, responses, cmd); + } + + private Integer fetchMultipleKeypairs(List responses, ListUserKeysCmd cmd) { + List users; + if (cmd.getUserId() != null) { + _accountService.validateCallingUserHasAccessToDesiredUser(cmd.getUserId()); + users = List.of(cmd.getUserId()); + } else { + User callerUser = CallContext.current().getCallingUser(); + users = cmd.listAll() && isAdmin(callerUser.getAccountId()) ? queryService.searchForAccessableUsers() : List.of(callerUser.getId()); + } + + Pair, Integer> keyPairs = apiKeyPairDao.listByUserIdsPaginated(users, cmd); + keyPairs.first().stream() + .filter(keyPair -> isAccessingKeypairSuperset(keyPair, cmd)) + .forEach(keyPair -> { + addKeypairResponse(keyPair, responses, cmd); + markExpiredKeysWithStateExpired(keyPair); + }); + + return keyPairs.second(); + } + + @Override + public List listKeyRules(ListUserKeyRulesCmd cmd) { + ApiKeyPair keyPair = apiKeyPairService.findById(cmd.getId()); + + validateKeyPairIsNotNull(keyPair); + _accountService.validateCallingUserHasAccessToDesiredUser(keyPair.getUserId()); + validateAccessingKeyPairPermissionsIsSupersetOfAccessedKeyPair(keyPair, cmd); + + Account account = _accountDao.findById(keyPair.getAccountId()); + + return apiKeyPairService.findAllPermissionsByKeyPairId(keyPair.getId(), account.getRoleId()); + } + + private void validateKeyPairIsNotNull(ApiKeyPair keyPair) { + if (keyPair == null) { + logger.info("Keypair not found."); + throw new InvalidParameterValueException("Could not complete request."); + } + } + + private void validateAccessingKeyPairPermissionsIsSupersetOfAccessedKeyPair(ApiKeyPair keyPair, BaseCmd cmd) { + if (!isAccessingKeypairSuperset(keyPair, cmd)) { + logger.info("Accessing API keypair has less permissions than accessed API keypair."); + throw new PermissionDeniedException("Could not complete request."); + } + } + + private Boolean isAccessingKeypairSuperset(ApiKeyPair accessedKeyPair, BaseCmd cmd) { + String apiKey = getAccessingApiKey(cmd); + if (apiKey == null) { + return Boolean.TRUE; + } + ApiKeyPair accessingKeyPair = apiKeyPairService.findByApiKey(apiKey); + return isApiKeySupersetOfPermission(new ArrayList<>(getAllKeypairPermissions(accessingKeyPair.getApiKey())), new ArrayList<>(getAllKeypairPermissions(accessedKeyPair.getApiKey()))); + } + + @Override + public String getAccessingApiKey(BaseCmd cmd) { + try { + if (cmd instanceof BaseAsyncCmd && ((BaseAsyncCmd) cmd).getJob().toString().contains("signature")) { + return parseApiKeyFromAsyncJob((BaseAsyncCmd) cmd); + } + boolean accessedByApiKey = cmd.getFullUrlParams().containsKey(ApiConstants.SIGNATURE); + String accessingApiKey = cmd.getFullUrlParams().get("apiKey"); + if (accessedByApiKey) { + return accessingApiKey; + } + } catch (NullPointerException e) { + logger.info("Accessing API through session."); + } + return null; + } + + private String parseApiKeyFromAsyncJob(BaseAsyncCmd cmd) { + String jobString = cmd.getJob().toString(); + int indexOfApiKey = jobString.indexOf("\"", jobString.indexOf("apiKey")) + 1; + int indexValueOfApiKey = jobString.indexOf("\"", indexOfApiKey) + 1; + return jobString.substring(indexValueOfApiKey, jobString.indexOf("\"", indexValueOfApiKey)).replace("\\", ""); + } + + + private Boolean isApiKeySupersetOfPermission(List baseKeyPairPermissions, List comparedPermissions) { + Map apiNameToBaseKeyPermissions = roleService.getRoleRulesAndPermissions(baseKeyPairPermissions); + + return roleService.roleHasPermission(apiNameToBaseKeyPermissions, comparedPermissions); + } + + private void markExpiredKeysWithStateExpired(ApiKeyPair apiKeyPair) { + if (apiKeyPair.hasEndDatePassed()) { + internalDeleteApiKey(apiKeyPair); + } + } + + public void deleteApiKey(DeleteUserKeysCmd cmd) { + ApiKeyPair keyPair = apiKeyPairService.findById(cmd.getId()); + if (keyPair == null) { + throw new InvalidParameterValueException(String.format("No keypair found with the id [%s].", cmd.getId())); + } + _accountService.validateCallingUserHasAccessToDesiredUser(keyPair.getUserId()); + + deleteApiKey(keyPair); + } + + @Override + public void validateCallingUserHasAccessToDesiredUser(Long userId) { + User callerUser = CallContext.current().getCallingUser(); + if (!isAdmin(callerUser.getAccountId()) && callerUser.getId() != userId) { + throw new PermissionDeniedException("Only admins can operate on API keys owned by other users"); + } + List accessibleUsers = queryService.searchForAccessableUsers(); + User desiredUser = _userDao.getUser(userId); + if (accessibleUsers.stream().noneMatch(u -> Objects.equals(u, userId))) { + throw new PermissionDeniedException(String.format("Could not perform operation because calling user has less permissions " + + "than the informed user [%s].", desiredUser.getId())); + } + } + + @Override + public void deleteApiKey(ApiKeyPair keyPair) { + User user = _userDao.findByIdIncludingRemoved(keyPair.getUserId()); + if (user == null) { + throw new InvalidParameterValueException("User associated to the key does not exist."); + } + + if ((BaremetalUtils.BAREMETAL_SYSTEM_ACCOUNT_NAME.equals(user.getUsername()) || user.getId() == User.UID_SYSTEM) + && Boolean.parseBoolean(_configDao.getValue(Config.BaremetalProvisionDoneNotificationEnabled.key()))) { + throw new PermissionDeniedException(String.format("User ID [%s] is system account and global setting " + + "baremetal.provision.done.notification is enabled, deletion of API Keys is not allowed. If you wish to delete " + + "the baremetal user/account or their API Key, please disable the baremetal.provision.done.notification configuration.", user.getUuid())); + } + internalDeleteApiKey(keyPair); + } + + private void internalDeleteApiKey(ApiKeyPair keyPair) { + List permissions = apiKeyPairPermissionsDao.findAllByApiKeyPairId(keyPair.getId()); + for (ApiKeyPairPermission permission : permissions) { + apiKeyPairPermissionsDao.remove(permission.getId()); + } + apiKeyPairDao.remove(keyPair.getId()); + } + + private void addKeypairResponse(ApiKeyPair keyPair, List responses, ListUserKeysCmd cmd) { + if (keyPair == null) { + return; + } + ApiKeyPairResponse response = cmd._responseGenerator.createKeyPairResponse(keyPair); + if (Boolean.TRUE.equals(cmd.getShowPermissions())) { + Account account = _accountDao.findById(keyPair.getAccountId()); + List apiKeyPairPermissions = apiKeyPairService.findAllPermissionsByKeyPairId(keyPair.getId(), account.getRoleId()); + response.setPermissions(apiKeyPairPermissions.stream().map(apiKeyPairPermission -> { + BaseRolePermissionResponse rolePermissionResponse = new BaseRolePermissionResponse(); + rolePermissionResponse.setRule(apiKeyPairPermission.getRule()); + rolePermissionResponse.setDescription(apiKeyPairPermission.getDescription()); + rolePermissionResponse.setRulePermission(apiKeyPairPermission.getPermission()); + + return rolePermissionResponse; + }).collect(Collectors.toList())); + } + response.setObjectName(ApiConstants.USER_API_KEY); + responses.add(response); + } + + @Override + public ApiKeyPair getKeyPairById(Long id) { + return apiKeyPairDao.findById(id); + } + protected void preventRootDomainAdminAccessToRootAdminKeys(User caller, ControlledEntity account) { if (isDomainAdminForRootDomain(caller) && isRootAdmin(account.getAccountId())) { String msg = String.format("Caller Username %s does not have access to root admin keys", caller.getUsername()); @@ -3086,6 +3358,11 @@ protected boolean isDomainAdminForRootDomain(User callingUser) { return caller.getType() == Account.Type.DOMAIN_ADMIN && caller.getDomainId() == Domain.ROOT_DOMAIN; } + @Override + public ApiKeyPair getKeyPairByApiKey(String apiKey) { + return apiKeyPairDao.findByApiKey(apiKey); + } + @Override public List listUserTwoFactorAuthenticationProviders() { return userTwoFactorAuthenticationProviders; @@ -3110,39 +3387,46 @@ public UserTwoFactorAuthenticator getUserTwoFactorAuthenticationProvider(final S @Override @DB @ActionEvent(eventType = EventTypes.EVENT_REGISTER_FOR_SECRET_API_KEY, eventDescription = "register for the developer API keys") - public String[] createApiKeyAndSecretKey(RegisterCmd cmd) { + public ApiKeyPair createApiKeyAndSecretKey(RegisterUserKeysCmd cmd) { Account caller = getCurrentCallingAccount(); - final Long userId = cmd.getId(); - - User user = getUserIncludingRemoved(userId); + User user = _userDao.findById(cmd.getUserId()); if (user == null) { - throw new InvalidParameterValueException("unable to find user by id"); + throw new InvalidParameterValueException(String.format("Unable to find user by id: %d", cmd.getUserId())); } + final String name = cmd.getName(); + final long userId = user.getId(); + final String description = cmd.getDescription(); + final Date startDate = cmd.getStartDate(); + final Date endDate = cmd.getEndDate(); + final List> rules = cmd.getRules(); + final RegisterUserKeysCmd registerCmd = cmd; + Account account = _accountDao.findById(user.getAccountId()); checkAccess(caller, null, true, account); verifyCallerPrivilegeForUserOrAccountOperations(user); - // don't allow updating system user - if (user.getId() == User.UID_SYSTEM) { - throw new PermissionDeniedException(String.format("user: %s is system account, update is not allowed", user)); + // don't allow baremetal or system user + if (BaremetalUtils.BAREMETAL_SYSTEM_ACCOUNT_NAME.equals(user.getUsername()) || user.getId() == User.UID_SYSTEM) { + throw new PermissionDeniedException(String.format("User id: [%s] is system account, update is not allowed.", user.getId())); } - // don't allow baremetal system user - if (BaremetalUtils.BAREMETAL_SYSTEM_ACCOUNT_NAME.equals(user.getUsername())) { - throw new PermissionDeniedException(String.format("user: %s is system account, update is not allowed", user)); + + Date now = DateTime.now().toDate(); + + if (endDate != null && endDate.compareTo(now) <= 0) { + throw new InvalidParameterValueException("Keypair cannot be created with expired date, please input a date on the future."); + } + if (ObjectUtils.allNotNull(startDate, endDate) && startDate.compareTo(endDate) > -1) { + throw new InvalidParameterValueException("Please specify an end date that is after the start date."); } - // generate both an api key and a secret key, update the user table with the keys, return the keys to the user - final String[] keys = new String[2]; - Transaction.execute(new TransactionCallbackNoReturn() { - @Override - public void doInTransactionWithoutResult(TransactionStatus status) { - keys[0] = createUserApiKey(userId); - keys[1] = createUserSecretKey(userId); - } + // generate both an api key and a secret key, return the keypair to the user + final ApiKeyPairVO newApiKeyPair = new ApiKeyPairVO(name, userId, description, startDate, endDate, account); + return Transaction.execute((TransactionCallback) status -> { + createUserApiKey(userId, newApiKeyPair); + createUserSecretKey(userId, newApiKeyPair); + return validateAndPersistKeyPairAndPermissions(account, newApiKeyPair, rules, registerCmd); }); - - return keys; } @Override @@ -3157,37 +3441,118 @@ public String[] createApiKeyAndSecretKey(final long userId) { Account account = _accountDao.findById(user.getAccountId()); checkAccess(caller, null, true, account); final String[] keys = new String[2]; + ApiKeyPairVO newTokenKeyPair = new ApiKeyPairVO(); + newTokenKeyPair.setName(String.valueOf(userId)); + newTokenKeyPair.setAccountId(user.getAccountId()); + newTokenKeyPair.setDomainId(account.getDomainId()); + newTokenKeyPair.setUserId(userId); + Transaction.execute(new TransactionCallbackNoReturn() { @Override public void doInTransactionWithoutResult(TransactionStatus status) { - keys[0] = AccountManagerImpl.this.createUserApiKey(userId); - keys[1] = AccountManagerImpl.this.createUserSecretKey(userId); + keys[0] = createUserApiKey(userId, newTokenKeyPair); + keys[1] = createUserSecretKey(userId, newTokenKeyPair); + apiKeyPairDao.persist(newTokenKeyPair); } }); return keys; } - private String createUserApiKey(long userId) { - try { - UserVO updatedUser = _userDao.createForUpdate(); + /*** + * Validates if the Keypair has at least one rule, then gets all account role permissions and calls a method that + * validates if the user permissions are a superset of permissions of the Keypair that is being created + * @param account is the user's account, from which the default permissions are pulled. + * @param newApiKeyPair is the new keypair being created + * @param rules are the rules passed to the API which are being validated, if no rules were passed, defaults to all + * account permissions + * @throws InvalidParameterValueException if the user's permissions are not a superset of the Keypair, or there are + * no rules associated with the Keypair + */ + @DB + private ApiKeyPairVO validateAndPersistKeyPairAndPermissions(Account account, ApiKeyPairVO newApiKeyPair, + List> rules, RegisterUserKeysCmd cmd) { + // this is only used to determine if we should use api key permissions or account permissions to base our new key on + String accessingApiKey = getAccessingApiKey(cmd); + if (newApiKeyPair.getName() == null) { + User user = _userDao.findById(newApiKeyPair.getUserId()); + newApiKeyPair.setName(user.getUsername() + " - API Keypair"); + } + + final Role accountRole = roleService.findRole(account.getRoleId()); + List allPermissions = accessingApiKey == null ? getAllAccountRolePermissions(accountRole) : getAllKeypairPermissions(accessingApiKey); + + List permissions; + + if (CollectionUtils.isEmpty(rules)) { + permissions = allPermissions.stream() + .map(permission -> new ApiKeyPairPermissionVO(0, permission.getRule().toString(), permission.getPermission(), permission.getDescription())) + .collect(Collectors.toList()); + } else { + permissions = new ArrayList<>(); + for (Map ruleDetail : rules) { + String rule = ruleDetail.get(ApiConstants.RULE).toString(); + RolePermission.Permission rulePermission = (RolePermission.Permission) ruleDetail.get(ApiConstants.PERMISSION); + String ruleDescription = (String) ruleDetail.get(ApiConstants.DESCRIPTION); + permissions.add(new ApiKeyPairPermissionVO(0, rule, rulePermission, ruleDescription)); + } + if (!isApiKeySupersetOfPermission(allPermissions, permissions)) { + throw new InvalidParameterValueException(String.format("The keypair being created has a bigger set of permissions than the account [%s] that owns it. This is " + + "not allowed.", account.getUuid())); + } + } + + ApiKeyPairVO savedApiKeyPair = apiKeyPairDao.persist(newApiKeyPair); + permissions.forEach(permission -> { + ApiKeyPairPermissionVO permissionVO = (ApiKeyPairPermissionVO) permission; + permissionVO.setApiKeyPairId(savedApiKeyPair.getId()); + apiKeyPairPermissionsDao.persist(permissionVO); + }); + return savedApiKeyPair; + } + + /** + * Gets all account role permissions + * @param accountRole base account role of the user. + */ + private List getAllAccountRolePermissions(Role accountRole) { + List allAccountRolePermissions = roleService.findAllPermissionsBy(accountRole.getId()); + return allAccountRolePermissions.stream().map(permission -> (RolePermissionEntity) permission) + .collect(Collectors.toList()); + } + + /** + * Gets all API keypair permissions for the given apiKey + */ + private List getAllKeypairPermissions(String apiKey) { + if (apiKey == null) { + throw new InvalidParameterValueException("API key not present in URL, cannot fetch API key rules."); + } + ApiKeyPair apiKeyPair = keyPairManager.findByApiKey(apiKey); + Account account = _accountDao.findById(apiKeyPair.getAccountId()); + List allApiKeyRolePermissions = keyPairManager.findAllPermissionsByKeyPairId(apiKeyPair.getId(), account.getRoleId()); + return allApiKeyRolePermissions.stream().map(permission -> (RolePermissionEntity) permission) + .collect(Collectors.toList()); + } + + private String createUserApiKey(long userId, ApiKeyPairVO newApiKeyPair) { + try { String encodedKey; - Pair userAcct; + ApiKeyPair keyPair; int retryLimit = 10; do { // FIXME: what algorithm should we use for API keys? KeyGenerator generator = KeyGenerator.getInstance("HmacSHA1"); SecretKey key = generator.generateKey(); encodedKey = Base64.encodeBase64URLSafeString(key.getEncoded()); - userAcct = _accountDao.findUserAccountByApiKey(encodedKey); + keyPair = apiKeyPairDao.findByApiKey(encodedKey); retryLimit--; - } while ((userAcct != null) && (retryLimit >= 0)); + } while ((keyPair != null) && (retryLimit >= 0)); - if (userAcct != null) { + if (keyPair != null) { return null; } - updatedUser.setApiKey(encodedKey); - _userDao.update(userId, updatedUser); + newApiKeyPair.setApiKey(encodedKey); return encodedKey; } catch (NoSuchAlgorithmException ex) { logger.error("error generating secret key for user {}", _userAccountDao.findById(userId), ex); @@ -3195,26 +3560,24 @@ private String createUserApiKey(long userId) { return null; } - private String createUserSecretKey(long userId) { + private String createUserSecretKey(long userId, ApiKeyPairVO newApiKeyPair) { try { - UserVO updatedUser = _userDao.createForUpdate(); String encodedKey; int retryLimit = 10; - UserVO userBySecretKey; + ApiKeyPairVO keyPairVO; do { KeyGenerator generator = KeyGenerator.getInstance("HmacSHA1"); SecretKey key = generator.generateKey(); encodedKey = Base64.encodeBase64URLSafeString(key.getEncoded()); - userBySecretKey = _userDao.findUserBySecretKey(encodedKey); + keyPairVO = apiKeyPairDao.findBySecretKey(encodedKey); retryLimit--; - } while ((userBySecretKey != null) && (retryLimit >= 0)); + } while ((keyPairVO != null) && (retryLimit >= 0)); - if (userBySecretKey != null) { + if (keyPairVO != null) { return null; } - updatedUser.setSecretKey(encodedKey); - _userDao.update(userId, updatedUser); + newApiKeyPair.setSecretKey(encodedKey); return encodedKey; } catch (NoSuchAlgorithmException ex) { logger.error("error generating secret key for user {}", _userAccountDao.findById(userId), ex); @@ -3222,6 +3585,10 @@ private String createUserSecretKey(long userId) { return null; } + public ApiKeyPair getLatestUserKeyPair(Long userId) { + return ApiDBUtils.searchForLatestUserKeyPair(userId); + } + @Override public void buildACLSearchBuilder(SearchBuilder sb, Long domainId, boolean isRecursive, List permittedAccounts, ListProjectResourcesCriteria listProjectResourcesCriteria) { @@ -3428,7 +3795,8 @@ public void buildACLViewSearchCriteria(SearchCriteria details = _userDetailsDao.listDetailsKeyPairs(userId); - userAccount.setDetails(details); - + if (userAccount != null) { + Map details = _userDetailsDao.listDetailsKeyPairs(userId); + userAccount.setDetails(details); + } return userAccount; } diff --git a/server/src/main/java/org/apache/cloudstack/acl/ApiKeyPairManagerImpl.java b/server/src/main/java/org/apache/cloudstack/acl/ApiKeyPairManagerImpl.java new file mode 100644 index 000000000000..1a45ed8147fc --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/acl/ApiKeyPairManagerImpl.java @@ -0,0 +1,83 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.acl; + +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.user.User; +import com.cloud.user.dao.UserDao; +import com.cloud.utils.component.ManagerBase; +import java.util.stream.Collectors; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPairService; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPair; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPairPermission; +import org.apache.cloudstack.acl.dao.ApiKeyPairDao; +import org.apache.cloudstack.acl.dao.ApiKeyPairPermissionsDao; +import org.apache.cloudstack.acl.dao.RolePermissionsDao; +import org.apache.cloudstack.query.QueryService; +import org.apache.commons.collections.CollectionUtils; + +import javax.inject.Inject; +import java.util.List; +import java.util.Objects; + +public class ApiKeyPairManagerImpl extends ManagerBase implements ApiKeyPairService { + @Inject + private ApiKeyPairDao apiKeyPairDao; + @Inject + private ApiKeyPairPermissionsDao apiKeyPairPermissionsDao; + @Inject + private UserDao userDao; + @Inject + private QueryService queryService; + @Inject + private RolePermissionsDao rolePermissionsDao; + + @Override + public List findAllPermissionsByKeyPairId(Long apiKeyPairId, Long roleId) { + List allPermissions = apiKeyPairPermissionsDao.findAllByKeyPairIdSorted(apiKeyPairId); + if (CollectionUtils.isNotEmpty(allPermissions)) { + return allPermissions.stream().map(p -> (ApiKeyPairPermission) p).collect(Collectors.toList()); + } + return rolePermissionsDao.findAllByRoleIdSorted(roleId).stream().map(p -> { + ApiKeyPairPermissionVO permission = new ApiKeyPairPermissionVO(); + permission.setRule(p.getRule().getRuleString()); + permission.setDescription(p.getDescription()); + permission.setPermission(p.getPermission()); + return permission; + }).collect(Collectors.toList()); + } + + @Override + public ApiKeyPair findByApiKey(String apiKey) { + return apiKeyPairDao.findByApiKey(apiKey); + } + + @Override + public ApiKeyPair findById(Long id) { + return apiKeyPairDao.findById(id); + } + + @Override + public void validateCallingUserHasAccessToDesiredUser(Long userId) { + List accessableUsers = queryService.searchForAccessableUsers(); + User desiredUser = userDao.getUser(userId); + if (accessableUsers.stream().noneMatch(u -> Objects.equals(u, userId))) { + throw new InvalidParameterValueException(String.format("Could not perform operation because calling user has less permissions " + + "than the informed user [%s].", desiredUser.getId())); + } + } +} diff --git a/server/src/main/java/org/apache/cloudstack/acl/RoleManagerImpl.java b/server/src/main/java/org/apache/cloudstack/acl/RoleManagerImpl.java index d1ae1b44a51f..b1fbdd51c451 100644 --- a/server/src/main/java/org/apache/cloudstack/acl/RoleManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/acl/RoleManagerImpl.java @@ -449,20 +449,21 @@ public Pair, Integer> findRolesByName(String name, String keyword, St /** * Removes roles from the given list if the role has different or more permissions than the user's calling the method role */ - protected int removeRolesIfNeeded(List roles) { + @Override + public int removeRolesIfNeeded(List roles) { if (roles.isEmpty()) { return 0; } Long callerRoleId = getCurrentAccount().getRoleId(); - Map callerRolePermissions = getRoleRulesAndPermissions(callerRoleId); + Map callerRolePermissions = getRoleRulesAndPermissions(findAllRolePermissionsEntityBy(callerRoleId)); int count = 0; Iterator rolesIterator = roles.iterator(); while (rolesIterator.hasNext()) { Role role = rolesIterator.next(); - if (role.getId() == callerRoleId || roleHasPermission(callerRolePermissions, role)) { + if (role.getId() == callerRoleId || roleHasPermission(callerRolePermissions, findAllRolePermissionsEntityBy(role.getId()))) { continue; } @@ -473,17 +474,11 @@ protected int removeRolesIfNeeded(List roles) { return count; } - /** - * Checks if the role of the caller account has compatible permissions of the specified role. - * For each permission of the role of the caller, the target role needs to contain the same permission. - * - * @param sourceRolePermissions the permissions of the caller role. - * @param targetRole the role that the caller role wants to access. - * @return True if the role can be accessed with the given permissions; false otherwise. - */ - protected boolean roleHasPermission(Map sourceRolePermissions, Role targetRole) { + + @Override + public boolean roleHasPermission(Map rolePermissions, List rolePermissionsToAccess) { Set rulesAlreadyCompared = new HashSet<>(); - for (RolePermission rolePermission : findAllPermissionsBy(targetRole.getId())) { + for (RolePermissionEntity rolePermission : rolePermissionsToAccess) { boolean permissionIsRegex = rolePermission.getRule().getRuleString().contains("*"); for (String apiName : accountManager.getApiNameList()) { @@ -491,7 +486,7 @@ protected boolean roleHasPermission(Map sourceRolePermission continue; } - if (rolePermission.getPermission() == Permission.ALLOW && (!sourceRolePermissions.containsKey(apiName) || sourceRolePermissions.get(apiName) == Permission.DENY)) { + if (rolePermission.getPermission() == Permission.ALLOW && (!rolePermissions.containsKey(apiName) || rolePermissions.get(apiName) == Permission.DENY)) { return false; } @@ -506,32 +501,32 @@ protected boolean roleHasPermission(Map sourceRolePermission return true; } - /** - * Given a role ID, returns a {@link Map} containing the API name as the key and the {@link Permission} for the API as the value. - * - * @param roleId ID from role. - */ - public Map getRoleRulesAndPermissions(Long roleId) { + @Override + public Map getRoleRulesAndPermissions(List rolePermissions) { Map roleRulesAndPermissions = new HashMap<>(); - for (RolePermission rolePermission : findAllPermissionsBy(roleId)) { + for (RolePermissionEntity rolePermission : rolePermissions) { boolean permissionIsRegex = rolePermission.getRule().getRuleString().contains("*"); - for (String apiName : accountManager.getApiNameList()) { - if (!rolePermission.getRule().matches(apiName)) { - continue; - } + mapRolePermissionToApiNames(rolePermission, roleRulesAndPermissions, permissionIsRegex); + } + return roleRulesAndPermissions; + } - if (!roleRulesAndPermissions.containsKey(apiName)) { - roleRulesAndPermissions.put(apiName, rolePermission.getPermission()); - } + private void mapRolePermissionToApiNames(RolePermissionEntity rolePermission, Map roleRulesAndPermissions, boolean permissionIsRegex) { + for (String apiName : accountManager.getApiNameList()) { + if (!rolePermission.getRule().matches(apiName)) { + continue; + } - if (!permissionIsRegex) { - break; - } + if (!roleRulesAndPermissions.containsKey(apiName)) { + roleRulesAndPermissions.put(apiName, rolePermission.getPermission()); + } + + if (!permissionIsRegex) { + break; } } - return roleRulesAndPermissions; } @Override @@ -571,6 +566,15 @@ public List findAllPermissionsBy(final Long roleId) { return Collections.emptyList(); } + @Override + public List findAllRolePermissionsEntityBy(final Long roleId) { + List permissions = rolePermissionsDao.findAllByRoleIdSorted(roleId); + if (permissions != null) { + return new ArrayList<>(permissions); + } + return Collections.emptyList(); + } + private boolean isCallerRootAdmin() { return accountManager.isRootAdmin(getCurrentAccount().getId()); } diff --git a/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml b/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml index 6edf206709c7..00e35fdfcfd0 100644 --- a/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml +++ b/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml @@ -41,6 +41,8 @@ + + diff --git a/server/src/test/java/com/cloud/network/as/AutoScaleManagerImplTest.java b/server/src/test/java/com/cloud/network/as/AutoScaleManagerImplTest.java index 7d2b35361bca..686af815329f 100644 --- a/server/src/test/java/com/cloud/network/as/AutoScaleManagerImplTest.java +++ b/server/src/test/java/com/cloud/network/as/AutoScaleManagerImplTest.java @@ -76,6 +76,9 @@ import org.mockito.junit.MockitoJUnitRunner; import org.springframework.test.util.ReflectionTestUtils; +import com.cloud.api.ApiDBUtils; +import org.apache.cloudstack.acl.ApiKeyPairVO; +import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService; import com.cloud.agent.AgentManager; import com.cloud.agent.api.PerformanceMonitorAnswer; import com.cloud.agent.api.PerformanceMonitorCommand; @@ -263,9 +266,14 @@ public class AutoScaleManagerImplTest { @Mock VirtualMachineManager virtualMachineManager; + @Mock + NetworkOrchestrationService networkOrchestrationService; + AccountVO account; UserVO user; + MockedStatic mockedApiDBUtils; + final static String INVALID = "invalid"; private static final Long counterId = 1L; @@ -416,6 +424,11 @@ public void setUp() { Mockito.doNothing().when(accountManager).checkAccess(Mockito.any(Account.class), Mockito.isNull(), Mockito.anyBoolean(), Mockito.any()); when(asPolicyDao.persist(any(AutoScalePolicyVO.class))).thenReturn(asScaleUpPolicyMock); + mockedApiDBUtils = Mockito.mockStatic(ApiDBUtils.class); + ApiKeyPairVO ret = new ApiKeyPairVO(); + ret.setSecretKey("secretkey"); + ret.setApiKey("apikey"); + when(ApiDBUtils.searchForLatestUserKeyPair(Mockito.any())).thenReturn(ret); userDataDetails.put("0", new HashMap<>() {{ put("key1", "value1"); put("key2", "value2"); }}); Mockito.doReturn(userDataFinal).when(userVmMgr).finalizeUserData(any(), any(), any()); @@ -424,6 +437,7 @@ public void setUp() { @After public void tearDown() { + mockedApiDBUtils.close(); CallContext.unregister(); } @@ -871,9 +885,6 @@ public void testDeleteAutoScaleVmProfileFail() { public void testCheckAutoScaleUserSucceed() throws NoSuchFieldException, IllegalAccessException { when(userDao.findById(any())).thenReturn(userMock); when(userMock.getAccountId()).thenReturn(accountId); - when(userMock.getApiKey()).thenReturn(autoScaleUserApiKey); - when(userMock.getSecretKey()).thenReturn(autoScaleUserSecretKey); - final Field f = ConfigKey.class.getDeclaredField("_defaultValue"); f.setAccessible(true); f.set(ApiServiceConfiguration.ApiServletPath, "http://10.10.10.10:8080/client/api"); @@ -883,25 +894,6 @@ public void testCheckAutoScaleUserSucceed() throws NoSuchFieldException, Illegal @Test(expected = InvalidParameterValueException.class) public void testCheckAutoScaleUserFail1() { - when(userDao.findById(any())).thenReturn(userMock); - when(userMock.getAccountId()).thenReturn(accountId); - when(userMock.getApiKey()).thenReturn(autoScaleUserApiKey); - when(userMock.getSecretKey()).thenReturn(null); - - autoScaleManagerImplSpy.checkAutoScaleUser(autoScaleUserId, accountId); - } - - @Test(expected = InvalidParameterValueException.class) - public void testCheckAutoScaleUserFail2() { - when(userDao.findById(any())).thenReturn(userMock); - when(userMock.getAccountId()).thenReturn(accountId); - when(userMock.getApiKey()).thenReturn(null); - - autoScaleManagerImplSpy.checkAutoScaleUser(autoScaleUserId, accountId); - } - - @Test(expected = InvalidParameterValueException.class) - public void testCheckAutoScaleUserFail3() { when(userDao.findById(any())).thenReturn(userMock); when(userMock.getAccountId()).thenReturn(accountId + 1L); @@ -909,18 +901,16 @@ public void testCheckAutoScaleUserFail3() { } @Test(expected = InvalidParameterValueException.class) - public void testCheckAutoScaleUserFail4() { + public void testCheckAutoScaleUserFail2() { when(userDao.findById(any())).thenReturn(null); autoScaleManagerImplSpy.checkAutoScaleUser(autoScaleUserId, accountId); } @Test(expected = InvalidParameterValueException.class) - public void testCheckAutoScaleUserFail5() throws NoSuchFieldException, IllegalAccessException { + public void testCheckAutoScaleUserFail3() throws NoSuchFieldException, IllegalAccessException { when(userDao.findById(any())).thenReturn(userMock); when(userMock.getAccountId()).thenReturn(accountId); - when(userMock.getApiKey()).thenReturn(autoScaleUserApiKey); - when(userMock.getSecretKey()).thenReturn(autoScaleUserSecretKey); final Field f = ConfigKey.class.getDeclaredField("_defaultValue"); f.setAccessible(true); diff --git a/server/src/test/java/com/cloud/user/AccountManagerImplTest.java b/server/src/test/java/com/cloud/user/AccountManagerImplTest.java index 3055e48247c4..f673703b752d 100644 --- a/server/src/test/java/com/cloud/user/AccountManagerImplTest.java +++ b/server/src/test/java/com/cloud/user/AccountManagerImplTest.java @@ -19,11 +19,14 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.nullable; +import com.cloud.utils.Ternary; import java.net.InetAddress; import java.net.UnknownHostException; +import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; +import java.util.Date; import java.util.List; import java.util.Map; @@ -31,10 +34,24 @@ import org.apache.cloudstack.acl.Role; import org.apache.cloudstack.acl.RoleService; import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.acl.ApiKeyPairPermissionVO; +import org.apache.cloudstack.acl.ApiKeyPairVO; +import org.apache.cloudstack.acl.RolePermission; +import org.apache.cloudstack.acl.RolePermissionEntity; +import org.apache.cloudstack.acl.RolePermissionVO; +import org.apache.cloudstack.acl.RoleVO; import org.apache.cloudstack.acl.SecurityChecker.AccessType; import org.apache.cloudstack.api.command.admin.account.UpdateAccountCmd; + +import org.apache.cloudstack.acl.apikeypair.ApiKeyPair; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPairService; +import org.apache.cloudstack.acl.dao.ApiKeyPairDao; +import org.apache.cloudstack.acl.dao.ApiKeyPairPermissionsDao; +import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.command.admin.user.DeleteUserCmd; import org.apache.cloudstack.api.command.admin.user.GetUserKeysCmd; +import org.apache.cloudstack.api.command.admin.user.ListUserKeysCmd; +import org.apache.cloudstack.api.command.admin.user.RegisterUserKeysCmd; import org.apache.cloudstack.api.command.admin.user.UpdateUserCmd; import org.apache.cloudstack.api.response.UserTwoFactorAuthenticationSetupResponse; import org.apache.cloudstack.auth.UserAuthenticator; @@ -48,6 +65,7 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import static org.mockito.ArgumentMatchers.any; import org.mockito.ArgumentCaptor; import org.mockito.InOrder; import org.mockito.Mock; @@ -88,7 +106,9 @@ public class AccountManagerImplTest extends AccountManagetImplTestBase { @Mock private AccountService accountService; @Mock - private GetUserKeysCmd _listkeyscmd; + private GetUserKeysCmd _getkeyscmd; + @Mock + private ListUserKeysCmd listUserKeysCmd; @Mock private User _user; @Mock @@ -104,6 +124,15 @@ public class AccountManagerImplTest extends AccountManagetImplTestBase { @Mock private UserVO userVoMock; + @Mock + private ApiKeyPairService apiKeyPairService; + + @Mock + private ApiKeyPairVO apiKeyPairVOMock; + + @Mock + private Pair, Integer> pairMock; + private long accountMockId = 100l; @Mock @@ -130,9 +159,19 @@ public class AccountManagerImplTest extends AccountManagetImplTestBase { @Mock ConfigKey allowOperationsOnUsersInSameAccountMock; + @Mock RoleService roleService; + @Mock + ApiKeyPairPermissionsDao apiKeyPairPermissionsDaoMock; + + @Mock + ApiKeyPairDao apiKeyPairDaoMock; + + @Mock + RegisterUserKeysCmd registerCmdMock; + @Before public void setUp() throws Exception { enableUserTwoFactorAuthenticationMock = Mockito.mock(ConfigKey.class); @@ -152,6 +191,13 @@ public void beforeTest() { Mockito.doReturn(userVoIdMock).when(userVoMock).getId(); Mockito.lenient().doNothing().when(accountManagerImpl).checkRoleEscalation(accountMock, accountMock); + Mockito.doReturn(accountMockId).when(accountVoMock).getId(); + + Mockito.when(apiKeyPairDaoMock.persist(Mockito.any())).thenAnswer(i -> { + ApiKeyPairVO keyPair = (ApiKeyPairVO) i.getArguments()[0]; + keyPair.setId(1L); + return keyPair; + }); } @Test @@ -297,6 +343,7 @@ public void deleteUserTestIfUserIdIsNotEqualToCallerIdShouldNotThrowException() Mockito.doNothing().when(accountManagerImpl).checkAccountAndAccess(Mockito.any(), Mockito.any()); Mockito.doNothing().when(accountManagerImpl).verifyCallerPrivilegeForUserOrAccountOperations(userVoMock); + Mockito.doNothing().when(accountManagerImpl).removeUserApiKeys(Mockito.anyLong()); accountManagerImpl.deleteUser(cmd); } } @@ -337,11 +384,9 @@ public void testAuthenticateUser() throws UnknownHostException { @Test(expected = PermissionDeniedException.class) public void testgetUserCmd() { CallContext.register(callingUser, callingAccount); // Calling account is user account i.e normal account - Mockito.when(_listkeyscmd.getID()).thenReturn(1L); + Mockito.when(_getkeyscmd.getId()).thenReturn(1L); Mockito.when(accountManagerImpl.getActiveUser(1L)).thenReturn(userVoMock); Mockito.when(userAccountDaoMock.findById(1L)).thenReturn(userAccountVO); - Mockito.when(userAccountVO.getAccountId()).thenReturn(1L); - Mockito.lenient().when(accountManagerImpl.getAccount(Mockito.anyLong())).thenReturn(accountMock); // Queried account - admin account Mockito.lenient().when(callingUser.getAccountId()).thenReturn(1L); Mockito.lenient().when(_accountDao.findById(1L)).thenReturn(callingAccount); @@ -349,13 +394,13 @@ public void testgetUserCmd() { Mockito.lenient().when(accountService.isNormalUser(Mockito.anyLong())).thenReturn(Boolean.TRUE); Mockito.lenient().when(accountMock.getAccountId()).thenReturn(2L); - accountManagerImpl.getKeys(_listkeyscmd); + accountManagerImpl.getKeys(_getkeyscmd); } @Test(expected = PermissionDeniedException.class) public void testGetUserKeysCmdDomainAdminRootAdminUser() { CallContext.register(callingUser, callingAccount); - Mockito.when(_listkeyscmd.getID()).thenReturn(2L); + Mockito.when(_getkeyscmd.getId()).thenReturn(2L); Mockito.when(accountManagerImpl.getActiveUser(2L)).thenReturn(userVoMock); Mockito.when(userAccountDaoMock.findById(2L)).thenReturn(userAccountVO); Mockito.when(userAccountVO.getAccountId()).thenReturn(2L); @@ -378,7 +423,7 @@ public void testGetUserKeysCmdDomainAdminRootAdminUser() { Mockito.lenient().when(accountService.isDomainAdmin(Mockito.anyLong())).thenReturn(Boolean.TRUE); Mockito.lenient().when(accountMock.getAccountId()).thenReturn(2L); - accountManagerImpl.getKeys(_listkeyscmd); + accountManagerImpl.getKeys(_getkeyscmd); } @Test @@ -435,7 +480,7 @@ private void prepareMockAndExecuteUpdateUserTest(int numberOfExpectedCallsForSet Mockito.doReturn("password").when(UpdateUserCmdMock).getPassword(); Mockito.doReturn("newpassword").when(UpdateUserCmdMock).getCurrentPassword(); Mockito.doReturn(userVoMock).when(accountManagerImpl).retrieveAndValidateUser(UpdateUserCmdMock); - Mockito.doNothing().when(accountManagerImpl).validateAndUpdateApiAndSecretKeyIfNeeded(UpdateUserCmdMock, userVoMock); + Mockito.doReturn(apiKeyPairVOMock).when(accountManagerImpl).validateAndUpdateApiAndSecretKeyIfNeeded(UpdateUserCmdMock, userVoMock); Mockito.doReturn(accountMock).when(accountManagerImpl).retrieveAndValidateAccount(userVoMock); Mockito.doNothing().when(accountManagerImpl).validateAndUpdateFirstNameIfNeeded(UpdateUserCmdMock, userVoMock); @@ -517,7 +562,7 @@ public void validateAndUpdatApiAndSecretKeyIfNeededTestApiKeyAlreadyUsedBySomeon User otherUserMock = Mockito.mock(User.class); Mockito.doReturn(2L).when(otherUserMock).getId(); - Pair pairUserAccountMock = new Pair(otherUserMock, Mockito.mock(Account.class)); + Ternary pairUserAccountMock = new Ternary(otherUserMock, Mockito.mock(Account.class), apiKeyPairVOMock); Mockito.doReturn(pairUserAccountMock).when(_accountDao).findUserAccountByApiKey(apiKey); accountManagerImpl.validateAndUpdateApiAndSecretKeyIfNeeded(UpdateUserCmdMock, userVoMock); @@ -536,14 +581,15 @@ public void validateAndUpdatApiAndSecretKeyIfNeededTest() { User otherUserMock = Mockito.mock(User.class); Mockito.doReturn(1L).when(otherUserMock).getId(); - Pair pairUserAccountMock = new Pair(otherUserMock, Mockito.mock(Account.class)); + Ternary pairUserAccountMock = new Ternary<>(otherUserMock, Mockito.mock(Account.class), apiKeyPairVOMock); Mockito.doReturn(pairUserAccountMock).when(_accountDao).findUserAccountByApiKey(apiKey); + Mockito.doReturn(accountVoMock).when(_accountDao).findById(Mockito.anyLong()); accountManagerImpl.validateAndUpdateApiAndSecretKeyIfNeeded(UpdateUserCmdMock, userVoMock); Mockito.verify(_accountDao).findUserAccountByApiKey(apiKey); - Mockito.verify(userVoMock).setApiKey(apiKey); - Mockito.verify(userVoMock).setSecretKey(secretKey); + Mockito.verify(apiKeyPairVOMock).setApiKey(apiKey); + Mockito.verify(apiKeyPairVOMock).setSecretKey(secretKey); } @Test @@ -1239,6 +1285,344 @@ public void testGetActiveUserAccountByEmail() { Assert.assertEquals(userAccountVOList.get(0), userAccounts.get(0)); } + @Test + public void createApiKeyAndSecretKeyTestWithEmptyRules() { + CallContext.register(callingUser, callingAccount); + Mockito.lenient().when(callingUser.getId()).thenReturn(111L); + long userId = 111L; + + Mockito.when(registerCmdMock.getRules()).thenReturn(List.of()); + Mockito.when(registerCmdMock.getUserId()).thenReturn(userId); + + Mockito.when(userDaoMock.findById(any())).thenReturn(userVoMock); + Mockito.when(_accountDao.findById(Mockito.anyLong())).thenReturn(accountVoMock); + Mockito.doNothing().when(accountManagerImpl).checkAccess(Mockito.any(Account.class), Mockito.isNull(), Mockito.anyBoolean(), Mockito.any(Account.class)); + Mockito.when(apiKeyPairDaoMock.findBySecretKey(Mockito.anyString())).thenReturn(null); + Mockito.when(roleService.findAllPermissionsBy(Mockito.anyLong())).thenReturn(List.of( + new RolePermissionVO(1L, "api2", RolePermissionEntity.Permission.ALLOW, "description") + )); + Mockito.when(roleService.findRole(Mockito.anyLong())).thenReturn(new RoleVO()); + + accountManagerImpl.createApiKeyAndSecretKey(registerCmdMock); + Mockito.verify(apiKeyPairDaoMock, Mockito.times(0)).remove(Mockito.anyLong()); + Mockito.verify(apiKeyPairPermissionsDaoMock, Mockito.times(1)).persist(Mockito.any(ApiKeyPairPermissionVO.class)); + } + + @Test(expected = InvalidParameterValueException.class) + public void createApiKeyAndSecretKeyTestPermissionNotPresentOnAccount() { + CallContext.register(callingUser, callingAccount); + Mockito.lenient().when(callingUser.getId()).thenReturn(111L); + long userId = 111L; + + List> rules = new ArrayList<>(); + rules.add(Map.of( + ApiConstants.RULE, "api1", + ApiConstants.PERMISSION, RolePermission.Permission.ALLOW, + ApiConstants.DESCRIPTION, "description" + )); + Mockito.when(registerCmdMock.getRules()).thenReturn(rules); + Mockito.when(registerCmdMock.getUserId()).thenReturn(userId); + + ApiKeyPairVO apiKeyPairVO = new ApiKeyPairVO(); + apiKeyPairVO.setUserId(userId); + apiKeyPairVO.setId(1L); + + Mockito.when(userDaoMock.findById(any())).thenReturn(userVoMock); + Mockito.when(_accountDao.findById(Mockito.anyLong())).thenReturn(accountVoMock); + Mockito.doNothing().when(accountManagerImpl).checkAccess(Mockito.any(Account.class), Mockito.isNull(), Mockito.anyBoolean(), Mockito.any()); + Mockito.when(roleService.findAllPermissionsBy(Mockito.anyLong())).thenReturn(List.of( + new RolePermissionVO(1L, "api2", RolePermissionEntity.Permission.ALLOW, "description") + )); + Mockito.when(roleService.findRole(Mockito.anyLong())).thenReturn(new RoleVO()); + + accountManagerImpl.createApiKeyAndSecretKey(registerCmdMock); + Mockito.verify(apiKeyPairDaoMock, Mockito.times(1)).remove(Mockito.anyLong()); + Mockito.verify(apiKeyPairPermissionsDaoMock, Mockito.times(0)).persist(Mockito.any(ApiKeyPairPermissionVO.class)); + } + + @Test(expected = InvalidParameterValueException.class) + public void createApiKeyAndSecretTestKeyDeniedOnAccount() { + CallContext.register(callingUser, callingAccount); + Mockito.lenient().when(callingUser.getId()).thenReturn(111L); + long userId = 111L; + + List> rules = new ArrayList<>(); + rules.add(Map.of( + ApiConstants.RULE, "api", + ApiConstants.PERMISSION, RolePermission.Permission.ALLOW, + ApiConstants.DESCRIPTION, "description" + )); + Mockito.when(registerCmdMock.getRules()).thenReturn(rules); + Mockito.when(registerCmdMock.getUserId()).thenReturn(userId); + + ApiKeyPairVO apiKeyPairVO = new ApiKeyPairVO(); + apiKeyPairVO.setUserId(userId); + apiKeyPairVO.setId(1L); + + Mockito.when(userDaoMock.findById(Mockito.anyLong())).thenReturn(userVoMock); + Mockito.when(_accountDao.findById(Mockito.anyLong())).thenReturn(accountVoMock); + Mockito.doNothing().when(accountManagerImpl).checkAccess(Mockito.any(Account.class), Mockito.isNull(), Mockito.anyBoolean(), Mockito.any()); + Mockito.when(apiKeyPairDaoMock.findBySecretKey(Mockito.anyString())).thenReturn(null); + Mockito.when(roleService.findAllPermissionsBy(Mockito.anyLong())).thenReturn(List.of( + new RolePermissionVO(1L, "api", RolePermissionEntity.Permission.DENY, "description") + )); + Mockito.when(roleService.findRole(Mockito.anyLong())).thenReturn(new RoleVO()); + + accountManagerImpl.createApiKeyAndSecretKey(registerCmdMock); + Mockito.verify(apiKeyPairDaoMock, Mockito.times(1)).remove(Mockito.anyLong()); + Mockito.verify(apiKeyPairPermissionsDaoMock, Mockito.times(0)).persist(Mockito.any(ApiKeyPairPermissionVO.class)); + } + + @Test + public void createApiKeyAndSecretKeyTestAllowedOnAccount() { + CallContext.register(callingUser, callingAccount); + Mockito.lenient().when(callingUser.getId()).thenReturn(111L); + long userId = callingUser.getId(); + + List> rules = new ArrayList<>(); + rules.add(Map.of( + ApiConstants.RULE, "api", + ApiConstants.PERMISSION, RolePermission.Permission.ALLOW, + ApiConstants.DESCRIPTION, "description" + )); + Mockito.when(registerCmdMock.getRules()).thenReturn(rules); + Mockito.when(registerCmdMock.getUserId()).thenReturn(userId); + + ApiKeyPairPermissionVO permissionVO = new ApiKeyPairPermissionVO(); + ApiKeyPairVO apiKeyPairVO = new ApiKeyPairVO(); + apiKeyPairVO.setUserId(userId); + apiKeyPairVO.setId(1L); + + Mockito.when(userDaoMock.findById(Mockito.anyLong())).thenReturn(userVoMock); + Mockito.when(_accountDao.findById(Mockito.anyLong())).thenReturn(accountVoMock); + Mockito.doNothing().when(accountManagerImpl).checkAccess(Mockito.any(Account.class), Mockito.isNull(), Mockito.anyBoolean(), Mockito.any(Account.class)); + Mockito.when(apiKeyPairPermissionsDaoMock.persist(Mockito.any(ApiKeyPairPermissionVO.class))).thenReturn(permissionVO); + Mockito.doReturn(true).when(roleService).roleHasPermission(Mockito.any(), Mockito.any()); + Mockito.when(roleService.findRole(Mockito.anyLong())).thenReturn(new RoleVO()); + + Assert.assertEquals((long) accountManagerImpl.createApiKeyAndSecretKey(registerCmdMock).getUserId(), userId); + Mockito.verify(apiKeyPairPermissionsDaoMock, Mockito.times(1)).persist(Mockito.any(ApiKeyPairPermissionVO.class)); + } + + + @Test + public void createApiAndSecretKeyTestWithNonEmptyDates() { + CallContext.register(callingUser, callingAccount); + Mockito.lenient().when(callingUser.getId()).thenReturn(111L); + Date startDate = Date.from(Instant.parse("2024-03-03T10:15:30.00Z")); + Date endDate = Date.from(Instant.parse("2124-03-04T10:15:30.00Z")); + long userId = 111L; + + List> rules = new ArrayList<>(); + rules.add(Map.of( + ApiConstants.RULE, "api", + ApiConstants.PERMISSION, RolePermission.Permission.ALLOW, + ApiConstants.DESCRIPTION, "description" + )); + Mockito.when(registerCmdMock.getRules()).thenReturn(rules); + Mockito.when(registerCmdMock.getUserId()).thenReturn(userId); + Mockito.when(registerCmdMock.getStartDate()).thenReturn(startDate); + Mockito.when(registerCmdMock.getEndDate()).thenReturn(endDate); + Mockito.when(registerCmdMock.getDescription()).thenReturn("key description"); + + UserVO mockUser = new UserVO(userId); + ApiKeyPairPermissionVO permissionVO = new ApiKeyPairPermissionVO(); + ApiKeyPairVO apiKeyPairVO = new ApiKeyPairVO(); + apiKeyPairVO.setUserId(userId); + apiKeyPairVO.setId(1L); + + Mockito.when(userDaoMock.findById(Mockito.anyLong())).thenReturn(userVoMock); + Mockito.when(_accountDao.findById(Mockito.anyLong())).thenReturn(accountVoMock); + Mockito.doNothing().when(accountManagerImpl).checkAccess(Mockito.any(Account.class), Mockito.isNull(), Mockito.anyBoolean(), Mockito.any(Account.class)); + Mockito.when(apiKeyPairPermissionsDaoMock.persist(Mockito.any(ApiKeyPairPermissionVO.class))).thenReturn(permissionVO); + Mockito.when(apiKeyPairDaoMock.findBySecretKey(Mockito.anyString())).thenReturn(null); + Mockito.doReturn(true).when(roleService).roleHasPermission(Mockito.any(), Mockito.any()); + Mockito.when(roleService.findRole(Mockito.anyLong())).thenReturn(new RoleVO()); + + ApiKeyPair response = accountManagerImpl.createApiKeyAndSecretKey(registerCmdMock); + Mockito.verify(apiKeyPairDaoMock, Mockito.times(1)).persist(Mockito.any(ApiKeyPairVO.class)); + Assert.assertEquals(userId, (long) response.getUserId()); + Assert.assertEquals(response.getStartDate(), startDate); + Assert.assertEquals(response.getEndDate(), endDate); + Assert.assertEquals("key description", response.getDescription()); + } + + @Test(expected = InvalidParameterValueException.class) + public void createApiAndSecretKeyTestWithExpiredDate() { + CallContext.register(callingUser, callingAccount); + Mockito.lenient().when(callingUser.getId()).thenReturn(111L); + + Date startDate = Date.from(Instant.parse("2024-03-01T10:15:30.00Z")); + Date endDate = Date.from(Instant.parse("2024-03-02T10:15:30.00Z")); + long userId = 111L; + + List> rules = new ArrayList<>(); + rules.add(Map.of( + ApiConstants.RULE, "api", + ApiConstants.PERMISSION, RolePermission.Permission.ALLOW, + ApiConstants.DESCRIPTION, "description" + )); + Mockito.when(registerCmdMock.getRules()).thenReturn(rules); + Mockito.when(registerCmdMock.getUserId()).thenReturn(userId); + Mockito.when(registerCmdMock.getStartDate()).thenReturn(startDate); + Mockito.when(registerCmdMock.getEndDate()).thenReturn(endDate); + Mockito.when(registerCmdMock.getDescription()).thenReturn("key description"); + + UserVO mockUser = new UserVO(userId); + ApiKeyPairPermissionVO permissionVO = new ApiKeyPairPermissionVO(); + ApiKeyPairVO apiKeyPairVO = new ApiKeyPairVO(); + apiKeyPairVO.setUserId(userId); + apiKeyPairVO.setId(1L); + + Mockito.when(userDaoMock.findById(Mockito.anyLong())).thenReturn(userVoMock); + Mockito.when(_accountDao.findById(Mockito.anyLong())).thenReturn(accountVoMock); + Mockito.doNothing().when(accountManagerImpl).checkAccess(Mockito.any(Account.class), Mockito.isNull(), Mockito.anyBoolean(), Mockito.any(Account.class)); + + ApiKeyPair response = accountManagerImpl.createApiKeyAndSecretKey(registerCmdMock); + Assert.assertEquals((long) response.getUserId(), userId); + Assert.assertEquals(response.getStartDate(), startDate); + Assert.assertEquals(response.getEndDate(), endDate); + Assert.assertEquals(response.getDescription(), "key description"); + } + + @Test(expected = InvalidParameterValueException.class) + public void createApiAndSecretKeyTestWithInvalidDate() { + CallContext.register(callingUser, callingAccount); + Mockito.lenient().when(callingUser.getId()).thenReturn(111L); + + Date endDate = Date.from(Instant.parse("2024-03-02T10:15:30.00Z")); // this test will break in 100 years :O + Date startDate = Date.from(Instant.parse("2024-10-02T10:15:30.00Z")); + long userId = 111L; + + List> rules = new ArrayList<>(); + rules.add(Map.of( + ApiConstants.RULE, "api", + ApiConstants.PERMISSION, RolePermission.Permission.ALLOW, + ApiConstants.DESCRIPTION, "description" + )); + Mockito.when(registerCmdMock.getRules()).thenReturn(rules); + Mockito.when(registerCmdMock.getUserId()).thenReturn(userId); + Mockito.when(registerCmdMock.getStartDate()).thenReturn(startDate); + Mockito.when(registerCmdMock.getEndDate()).thenReturn(endDate); + Mockito.when(registerCmdMock.getDescription()).thenReturn("key description"); + + UserVO mockUser = new UserVO(userId); + ApiKeyPairPermissionVO permissionVO = new ApiKeyPairPermissionVO(); + ApiKeyPairVO apiKeyPairVO = new ApiKeyPairVO(); + apiKeyPairVO.setUserId(userId); + apiKeyPairVO.setId(1L); + + Mockito.when(userDaoMock.findById(Mockito.anyLong())).thenReturn(userVoMock); + Mockito.when(_accountDao.findById(Mockito.anyLong())).thenReturn(accountVoMock); + Mockito.doNothing().when(accountManagerImpl).checkAccess(Mockito.any(Account.class), Mockito.isNull(), Mockito.anyBoolean(), Mockito.any(Account.class)); + + ApiKeyPair response = accountManagerImpl.createApiKeyAndSecretKey(registerCmdMock); + Assert.assertEquals(userId, (long) response.getUserId()); + Assert.assertEquals(response.getStartDate(), startDate); + Assert.assertEquals(response.getEndDate(), endDate); + Assert.assertEquals("key description", response.getDescription()); + } + + @Test(expected = InvalidParameterValueException.class) + public void createApiAndSecretKeyTestWithMultipleAllowedPermissionsOneDenied() { + CallContext.register(callingUser, callingAccount); + Mockito.lenient().when(callingUser.getId()).thenReturn(111L); + long userId = 111L; + + List> rules = new ArrayList<>(); + rules.add(Map.of( + ApiConstants.RULE, "api1", + ApiConstants.PERMISSION, RolePermission.Permission.ALLOW, + ApiConstants.DESCRIPTION, "description" + )); + rules.add(Map.of( + ApiConstants.RULE, "api2", + ApiConstants.PERMISSION, RolePermission.Permission.ALLOW, + ApiConstants.DESCRIPTION, "description" + )); + rules.add(Map.of( + ApiConstants.RULE, "api3", + ApiConstants.PERMISSION, RolePermission.Permission.ALLOW, + ApiConstants.DESCRIPTION, "description" + )); + Mockito.when(registerCmdMock.getRules()).thenReturn(rules); + Mockito.when(registerCmdMock.getUserId()).thenReturn(userId); + + UserVO mockUser = new UserVO(userId); + ApiKeyPairVO apiKeyPairVO = new ApiKeyPairVO(); + apiKeyPairVO.setUserId(userId); + apiKeyPairVO.setId(1L); + + Mockito.when(userDaoMock.findById(Mockito.anyLong())).thenReturn(userVoMock); + Mockito.when(_accountDao.findById(Mockito.anyLong())).thenReturn(accountVoMock); + Mockito.doNothing().when(accountManagerImpl).checkAccess(Mockito.any(Account.class), Mockito.isNull(), Mockito.anyBoolean(), Mockito.any(Account.class)); + Mockito.when(apiKeyPairDaoMock.findBySecretKey(Mockito.anyString())).thenReturn(null); + Mockito.when(roleService.findAllPermissionsBy(Mockito.anyLong())).thenReturn(List.of( + new RolePermissionVO(1L, "api1", RolePermissionEntity.Permission.ALLOW, "description-1"), + new RolePermissionVO(1L, "api2", RolePermissionEntity.Permission.ALLOW, "description-2"), + new RolePermissionVO(1L, "api3", RolePermissionEntity.Permission.DENY, "description-3") + )); + Mockito.when(roleService.findRole(Mockito.anyLong())).thenReturn(new RoleVO()); + + accountManagerImpl.createApiKeyAndSecretKey(registerCmdMock); + Mockito.verify(apiKeyPairDaoMock, Mockito.times(1)).remove(Mockito.anyLong()); + Mockito.verify(apiKeyPairPermissionsDaoMock, Mockito.times(0)).persist(Mockito.any(ApiKeyPairPermissionVO.class)); + } + + @Test(expected = InvalidParameterValueException.class) + public void createApiAndSecretKeyTestWithMultipleAllowedPermissionsOneDoesNotExist() { + CallContext.register(callingUser, callingAccount); + Mockito.lenient().when(callingUser.getId()).thenReturn(111L); + long userId = 111L; + + List> rules = new ArrayList<>(); + rules.add(Map.of( + ApiConstants.RULE, "api1", + ApiConstants.PERMISSION, RolePermission.Permission.ALLOW, + ApiConstants.DESCRIPTION, "description" + )); + rules.add(Map.of( + ApiConstants.RULE, "api2", + ApiConstants.PERMISSION, RolePermission.Permission.ALLOW, + ApiConstants.DESCRIPTION, "description" + )); + rules.add(Map.of( + ApiConstants.RULE, "api3", + ApiConstants.PERMISSION, RolePermission.Permission.DENY, + ApiConstants.DESCRIPTION, "description" + )); + Mockito.when(registerCmdMock.getRules()).thenReturn(rules); + Mockito.when(registerCmdMock.getUserId()).thenReturn(userId); + + ApiKeyPairVO apiKeyPairVO = new ApiKeyPairVO(); + apiKeyPairVO.setUserId(userId); + apiKeyPairVO.setId(1L); + + Mockito.when(userDaoMock.findById(Mockito.anyLong())).thenReturn(userVoMock); + Mockito.when(_accountDao.findById(Mockito.anyLong())).thenReturn(accountVoMock); + Mockito.doNothing().when(accountManagerImpl).checkAccess(Mockito.any(Account.class), Mockito.isNull(), Mockito.anyBoolean(), Mockito.any(Account.class)); + Mockito.when(apiKeyPairDaoMock.findBySecretKey(Mockito.anyString())).thenReturn(null); + Mockito.when(roleService.findAllPermissionsBy(Mockito.anyLong())).thenReturn(List.of( + new RolePermissionVO(1L, "api1", RolePermissionEntity.Permission.ALLOW, "description-1"), + new RolePermissionVO(1L, "api2", RolePermissionEntity.Permission.ALLOW, "description-2") + )); + Mockito.when(roleService.findRole(Mockito.anyLong())).thenReturn(new RoleVO()); + + accountManagerImpl.createApiKeyAndSecretKey(registerCmdMock); + Mockito.verify(apiKeyPairDaoMock, Mockito.times(0)).remove(Mockito.anyLong()); + Mockito.verify(apiKeyPairPermissionsDaoMock, Mockito.times(0)).persist(Mockito.any(ApiKeyPairPermissionVO.class)); + } + + @Test + public void validateAccountHasAccessToResourceTestValidatesAccessToControlledEntity() { + VMInstanceVO vmInstanceVo = new VMInstanceVO(); + Mockito.doNothing().when(accountManagerImpl).checkAccess(Mockito.any(), Mockito.any(), Mockito.anyBoolean(), Mockito.any()); + + accountManagerImpl.validateAccountHasAccessToResource(callingAccount, AccessType.UseEntry, vmInstanceVo); + + Mockito.verify(accountManagerImpl).checkAccess(callingAccount, AccessType.UseEntry, true, vmInstanceVo); + } + @Test public void testDeleteWebhooksForAccount() { try (MockedStatic mockedComponentContext = Mockito.mockStatic(ComponentContext.class)) { @@ -1506,6 +1890,15 @@ public void testAssertUserNotAlreadyInDomain_UserExistsInDiffDomain() { accountManagerImpl.assertUserNotAlreadyInDomain(existingUser, originalAccount); } + @Test + public void deleteApiKeyTestOnePermission() { + Mockito.when(apiKeyPairPermissionsDaoMock.findAllByApiKeyPairId(Mockito.any())).thenReturn(List.of(new ApiKeyPairPermissionVO())); + Mockito.when(userDaoMock.findByIdIncludingRemoved(Mockito.any())).thenReturn(new UserVO()); + accountManagerImpl.deleteApiKey(new ApiKeyPairVO(1L, 1L)); + Mockito.verify(apiKeyPairPermissionsDaoMock, Mockito.times(1)).remove(Mockito.anyLong()); + Mockito.verify(apiKeyPairDaoMock, Mockito.times(1)).remove(Mockito.anyLong()); + } + @Test public void testCheckCallerRoleTypeAllowedToUpdateUserSameAccount() { Mockito.lenient().when(accountManagerImpl.getCurrentCallingAccount()).thenReturn(accountMock); diff --git a/server/src/test/java/com/cloud/user/MockAccountManagerImpl.java b/server/src/test/java/com/cloud/user/MockAccountManagerImpl.java index 8c569fb3ec8d..25028dff7af6 100644 --- a/server/src/test/java/com/cloud/user/MockAccountManagerImpl.java +++ b/server/src/test/java/com/cloud/user/MockAccountManagerImpl.java @@ -25,18 +25,26 @@ import org.apache.cloudstack.acl.ControlledEntity; import org.apache.cloudstack.acl.RoleType; import org.apache.cloudstack.acl.SecurityChecker.AccessType; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPair; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPairPermission; +import org.apache.cloudstack.api.command.admin.user.RegisterUserKeysCmd; import org.apache.cloudstack.api.command.admin.account.CreateAccountCmd; import org.apache.cloudstack.api.command.admin.account.UpdateAccountCmd; import org.apache.cloudstack.api.command.admin.user.DeleteUserCmd; import org.apache.cloudstack.api.command.admin.user.GetUserKeysCmd; import org.apache.cloudstack.api.command.admin.user.MoveUserCmd; -import org.apache.cloudstack.api.command.admin.user.RegisterCmd; import org.apache.cloudstack.api.command.admin.user.UpdateUserCmd; +import org.apache.cloudstack.api.command.admin.user.DeleteUserKeysCmd; +import org.apache.cloudstack.api.command.admin.user.ListUserKeyRulesCmd; +import org.apache.cloudstack.api.command.admin.user.ListUserKeysCmd; +import org.apache.cloudstack.api.response.ApiKeyPairResponse; +import org.apache.cloudstack.api.response.ListResponse; import org.apache.cloudstack.api.response.UserTwoFactorAuthenticationSetupResponse; +import org.apache.cloudstack.api.BaseCmd; + import org.apache.cloudstack.auth.UserTwoFactorAuthenticator; import org.apache.cloudstack.framework.config.ConfigKey; import org.springframework.stereotype.Component; - import com.cloud.api.auth.SetupUserTwoFactorAuthenticationCmd; import com.cloud.api.query.vo.ControlledViewEntity; import com.cloud.dc.DataCenter; @@ -56,6 +64,7 @@ import com.cloud.utils.db.SearchBuilder; import com.cloud.utils.db.SearchCriteria; + @Component public class MockAccountManagerImpl extends ManagerBase implements Manager, AccountManager { @@ -297,6 +306,31 @@ public UserAccount getUserAccountById(Long userId) { return null; } + @Override + public Pair> getKeys(GetUserKeysCmd cmd) { + return null; + } + + @Override + public ListResponse listKeys(ListUserKeysCmd cmd) { + return null; + } + + @Override + public List listKeyRules(ListUserKeyRulesCmd cmd) { + return null; + } + + @Override + public void deleteApiKey(DeleteUserKeysCmd cmd) { + + } + + @Override + public void deleteApiKey(ApiKeyPair id) { + + } + @Override public void logoutUser(long userId) { // TODO Auto-generated method stub @@ -308,12 +342,12 @@ public UserAccount authenticateUser(String username, String password, Long domai } @Override - public Pair findUserByApiKey(String apiKey) { + public Ternary findUserByApiKey(String apiKey) { return null; } @Override - public String[] createApiKeyAndSecretKey(RegisterCmd cmd) { + public ApiKeyPair createApiKeyAndSecretKey(RegisterUserKeysCmd cmd) { return null; } @@ -445,6 +479,10 @@ public void validateAccountHasAccessToResource(Account account, AccessType acces // TODO Auto-generated method stub } + @Override + public void validateCallingUserHasAccessToDesiredUser(Long userId) { + } + @Override public Long finalyzeAccountId(String accountName, Long domainId, Long projectId, boolean enabledOnly) { // TODO Auto-generated method stub @@ -452,22 +490,32 @@ public Long finalyzeAccountId(String accountName, Long domainId, Long projectId, } @Override - public Pair> getKeys(GetUserKeysCmd cmd) { + public List listUserTwoFactorAuthenticationProviders() { return null; } @Override - public Pair> getKeys(Long userId) { + public UserTwoFactorAuthenticator getUserTwoFactorAuthenticationProvider(Long domainId) { return null; } @Override - public List listUserTwoFactorAuthenticationProviders() { + public ApiKeyPair getLatestUserKeyPair(Long userId) { return null; } @Override - public UserTwoFactorAuthenticator getUserTwoFactorAuthenticationProvider(Long domainId) { + public ApiKeyPair getKeyPairById(Long id) { + return null; + } + + @Override + public ApiKeyPair getKeyPairByApiKey(String apiKey) { + return null; + } + + @Override + public String getAccessingApiKey(BaseCmd cmd) { return null; } diff --git a/server/src/test/java/org/apache/cloudstack/acl/RoleManagerImplTest.java b/server/src/test/java/org/apache/cloudstack/acl/RoleManagerImplTest.java index 5d9ee268d8b7..7ff4320d41b5 100644 --- a/server/src/test/java/org/apache/cloudstack/acl/RoleManagerImplTest.java +++ b/server/src/test/java/org/apache/cloudstack/acl/RoleManagerImplTest.java @@ -90,17 +90,9 @@ public void setUpRoleVisibilityTests() { Mockito.doReturn(RolePermissionEntity.Permission.ALLOW).when(rolePermission1Mock).getPermission(); Mockito.doReturn(RolePermissionEntity.Permission.ALLOW).when(rolePermission2Mock).getPermission(); - List lessPermissionsRolePermissions = Collections.singletonList(rolePermission1Mock); Mockito.doReturn(1L).when(lessPermissionsRoleMock).getId(); - Mockito.when(roleManagerImpl.findAllPermissionsBy(1L)).thenReturn(lessPermissionsRolePermissions); - - List morePermissionsRolePermissions = List.of(rolePermission1Mock, rolePermission2Mock); Mockito.doReturn(2L).when(morePermissionsRoleMock).getId(); - Mockito.when(roleManagerImpl.findAllPermissionsBy(morePermissionsRoleMock.getId())).thenReturn(morePermissionsRolePermissions); - - List differentPermissionsRolePermissions = Collections.singletonList(rolePermission2Mock); Mockito.doReturn(3L).when(differentPermissionsRoleMock).getId(); - Mockito.when(roleManagerImpl.findAllPermissionsBy(differentPermissionsRoleMock.getId())).thenReturn(differentPermissionsRolePermissions); } @Before @@ -225,9 +217,6 @@ public void removeRolesIfNeededTestRoleWithMoreAndSamePermissionsKeepRoles() { setUpRoleVisibilityTests(); List roles = new ArrayList<>(); - List callerAccountRolePermissions = List.of(rolePermission1Mock, rolePermission2Mock); - Mockito.when(roleManagerImpl.findAllPermissionsBy(callerAccountRoleMock.getId())).thenReturn(callerAccountRolePermissions); - roles.add(callerAccountRoleMock); roles.add(lessPermissionsRoleMock); @@ -243,9 +232,11 @@ public void removeRolesIfNeededTestRoleWithLessPermissionsRemoveRoles() { setUpRoleVisibilityTests(); List roles = new ArrayList<>(); - List callerAccountRolePermissions = Collections.singletonList(rolePermission1Mock); - Mockito.when(roleManagerImpl.findAllPermissionsBy(callerAccountRoleMock.getId())).thenReturn(callerAccountRolePermissions); + List callerAccountRolePermissions = Collections.singletonList(rolePermission1Mock); + Mockito.when(roleManagerImpl.findAllRolePermissionsEntityBy(callerAccountRoleMock.getId())).thenReturn(callerAccountRolePermissions); + List callerAccountRolePermissions2 = Collections.singletonList(rolePermission2Mock); + Mockito.when(roleManagerImpl.findAllRolePermissionsEntityBy(morePermissionsRoleMock.getId())).thenReturn(callerAccountRolePermissions2); roles.add(callerAccountRoleMock); roles.add(morePermissionsRoleMock); @@ -261,8 +252,11 @@ public void removeRolesIfNeededTestRoleWithDifferentPermissionsRemoveRoles() { setUpRoleVisibilityTests(); List roles = new ArrayList<>(); - List callerAccountRolePermissions = Collections.singletonList(rolePermission1Mock); - Mockito.when(roleManagerImpl.findAllPermissionsBy(callerAccountRoleMock.getId())).thenReturn(callerAccountRolePermissions); + List callerAccountRolePermissions = Collections.singletonList(rolePermission1Mock); + Mockito.when(roleManagerImpl.findAllRolePermissionsEntityBy(callerAccountRoleMock.getId())).thenReturn(callerAccountRolePermissions); + + List callerAccountRolePermissions3 = Collections.singletonList(rolePermission2Mock); + Mockito.when(roleManagerImpl.findAllRolePermissionsEntityBy(differentPermissionsRoleMock.getId())).thenReturn(callerAccountRolePermissions3); roles.add(callerAccountRoleMock); roles.add(differentPermissionsRoleMock); @@ -279,7 +273,7 @@ public void roleHasPermissionTestRoleWithMoreAndSamePermissionsReturnsTrue() { rolePermissions.put("api1", Permission.ALLOW); rolePermissions.put("api2", Permission.ALLOW); - boolean result = roleManagerImpl.roleHasPermission(rolePermissions, lessPermissionsRoleMock); + boolean result = roleManagerImpl.roleHasPermission(rolePermissions, Collections.singletonList(rolePermission1Mock)); Assert.assertTrue(result); } @@ -290,7 +284,7 @@ public void roleHasPermissionTestRoleAllowedApisDoesNotContainRoleToAccessAllowe rolePermissions.put("api2", Permission.ALLOW); rolePermissions.put("api3", Permission.ALLOW); - boolean result = roleManagerImpl.roleHasPermission(rolePermissions, morePermissionsRoleMock); + boolean result = roleManagerImpl.roleHasPermission(rolePermissions, List.of(rolePermission1Mock, rolePermission2Mock)); Assert.assertFalse(result); } @@ -301,7 +295,7 @@ public void roleHasPermissionTestRolePermissionsDeniedApiContainRoleToAccessAllo rolePermissions.put("api1", Permission.ALLOW); rolePermissions.put("api2", Permission.DENY); - boolean result = roleManagerImpl.roleHasPermission(rolePermissions, morePermissionsRoleMock); + boolean result = roleManagerImpl.roleHasPermission(rolePermissions, List.of(rolePermission1Mock, rolePermission2Mock)); Assert.assertFalse(result); } @@ -310,7 +304,7 @@ public void roleHasPermissionTestRolePermissionsDeniedApiContainRoleToAccessAllo public void getRolePermissionsTestRoleReturnsRolePermissions() { setUpRoleVisibilityTests(); - Map roleRulesAndPermissions = roleManagerImpl.getRoleRulesAndPermissions(morePermissionsRoleMock.getId()); + Map roleRulesAndPermissions = roleManagerImpl.getRoleRulesAndPermissions(List.of(rolePermission1Mock, rolePermission2Mock)); Assert.assertEquals(2, roleRulesAndPermissions.size()); Assert.assertEquals(roleRulesAndPermissions.get("api1"), Permission.ALLOW); diff --git a/tools/marvin/marvin/cloudstackTestClient.py b/tools/marvin/marvin/cloudstackTestClient.py index 8c5a0d6e6123..311abb7d9292 100644 --- a/tools/marvin/marvin/cloudstackTestClient.py +++ b/tools/marvin/marvin/cloudstackTestClient.py @@ -151,16 +151,9 @@ def __createApiClient(self): getuser_keys = getUserKeys.getUserKeysCmd() getuser_keys.id = list_user_res[0].id getuser_keys_res = self.__apiClient.getUserKeys(getuser_keys) - if getuser_keys_res is None : - self.__logger.error("__createApiClient: API " - "Client Creation Failed") - return FAILED - - api_key = getuser_keys_res.apikey - security_key = getuser_keys_res.secretkey - user_id = list_user_res[0].id - if api_key is None: + + if getuser_keys_res is None: ret = self.__getKeys(user_id) if ret != FAILED: mgmt_details.apiKey = ret[0] @@ -172,8 +165,8 @@ def __createApiClient(self): return FAILED else: mgmt_details.port = 8080 - mgmt_details.apiKey = api_key - mgmt_details.securityKey = security_key + mgmt_details.apiKey = getuser_keys_res[0].apikey + mgmt_details.securityKey = getuser_keys_res[0].secretkey ''' Now Create the Connection objects and Api Client using new details @@ -216,6 +209,7 @@ def __getKeys(self, userid): try: register_user = registerUserKeys.registerUserKeysCmd() register_user.id = userid + register_user.name = f"keypair-{userid}" register_user_res = \ self.__apiClient.registerUserKeys(register_user) if not register_user_res: @@ -224,13 +218,13 @@ def __getKeys(self, userid): getuser_keys = getUserKeys.getUserKeysCmd() getuser_keys.id = userid getuser_keys_res = self.__apiClient.getUserKeys(getuser_keys) - if getuser_keys_res is None : + if getuser_keys_res is None: self.__logger.error("__createApiClient: API " "Client Creation Failed") return FAILED - api_key = getuser_keys_res.apikey - security_key = getuser_keys_res.secretkey + api_key = getuser_keys_res[0].apikey + security_key = getuser_keys_res[0].secretkey return (api_key, security_key) except Exception as e: self.__logger.exception("Exception Occurred Under __geKeys : " diff --git a/tools/marvin/marvin/lib/base.py b/tools/marvin/marvin/lib/base.py index 3d232d887c8d..539b7f6f5994 100755 --- a/tools/marvin/marvin/lib/base.py +++ b/tools/marvin/marvin/lib/base.py @@ -335,6 +335,7 @@ def list(cls, apiclient, **kwargs): def registerUserKeys(cls, apiclient, userid): cmd = registerUserKeys.registerUserKeysCmd() cmd.id = userid + cmd.name = f"keypair-{userid}" return apiclient.registerUserKeys(cmd) def update(self, apiclient, **kwargs): From 51cbdf40fac08463fa5f07b2bb0516ece2234440 Mon Sep 17 00:00:00 2001 From: Nicole Schmidt Date: Mon, 3 Feb 2025 10:51:45 -0300 Subject: [PATCH 02/17] Add missing stubbing and remove unnecessary ones --- .../test/java/com/cloud/user/AccountManagerImplTest.java | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/server/src/test/java/com/cloud/user/AccountManagerImplTest.java b/server/src/test/java/com/cloud/user/AccountManagerImplTest.java index f673703b752d..d086ce1970c4 100644 --- a/server/src/test/java/com/cloud/user/AccountManagerImplTest.java +++ b/server/src/test/java/com/cloud/user/AccountManagerImplTest.java @@ -382,11 +382,10 @@ public void testAuthenticateUser() throws UnknownHostException { } @Test(expected = PermissionDeniedException.class) - public void testgetUserCmd() { + public void testGetUserCmd() { CallContext.register(callingUser, callingAccount); // Calling account is user account i.e normal account Mockito.when(_getkeyscmd.getId()).thenReturn(1L); Mockito.when(accountManagerImpl.getActiveUser(1L)).thenReturn(userVoMock); - Mockito.when(userAccountDaoMock.findById(1L)).thenReturn(userAccountVO); Mockito.lenient().when(callingUser.getAccountId()).thenReturn(1L); Mockito.lenient().when(_accountDao.findById(1L)).thenReturn(callingAccount); @@ -402,9 +401,7 @@ public void testGetUserKeysCmdDomainAdminRootAdminUser() { CallContext.register(callingUser, callingAccount); Mockito.when(_getkeyscmd.getId()).thenReturn(2L); Mockito.when(accountManagerImpl.getActiveUser(2L)).thenReturn(userVoMock); - Mockito.when(userAccountDaoMock.findById(2L)).thenReturn(userAccountVO); - Mockito.when(userAccountVO.getAccountId()).thenReturn(2L); - Mockito.when(userDetailsDaoMock.listDetailsKeyPairs(Mockito.anyLong())).thenReturn(null); + Mockito.when(userVoMock.getAccountId()).thenReturn(2L); // Queried account - admin account AccountVO adminAccountMock = Mockito.mock(AccountVO.class); From 37160b94b6b907587ca9e6c342cce12561555ba8 Mon Sep 17 00:00:00 2001 From: Nicole Schmidt Date: Mon, 3 Feb 2025 15:14:55 -0300 Subject: [PATCH 03/17] Fix conflicts on checkAccess --- .../cloudstack/acl/DynamicRoleBasedAPIAccessChecker.java | 7 +++++-- .../acl/DynamicRoleBasedAPIAccessCheckerTest.java | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/plugins/acl/dynamic-role-based/src/main/java/org/apache/cloudstack/acl/DynamicRoleBasedAPIAccessChecker.java b/plugins/acl/dynamic-role-based/src/main/java/org/apache/cloudstack/acl/DynamicRoleBasedAPIAccessChecker.java index 85eeab03e7ab..4e1c19134ac7 100644 --- a/plugins/acl/dynamic-role-based/src/main/java/org/apache/cloudstack/acl/DynamicRoleBasedAPIAccessChecker.java +++ b/plugins/acl/dynamic-role-based/src/main/java/org/apache/cloudstack/acl/DynamicRoleBasedAPIAccessChecker.java @@ -160,8 +160,11 @@ public boolean checkAccess(User user, String commandName, ApiKeyPairPermission . logger.info("Account for user id {} is Root Admin or Domain Admin, all APIs are allowed.", user.getUuid()); return true; } - List allPermissions = roleAndPermissions.second(); - if (checkApiPermissionByRole(accountRole, commandName, allPermissions)) { + + List allRules = defineNewKeypairRules(accountRole, apiKeyPairPermissions); + boolean override = apiKeyPairPermissions.length != 0; + + if (checkApiPermissionByRole(accountRole, commandName, allRules, override)) { return true; } throw new UnavailableCommandException(String.format("The API [%s] does not exist or is not available for the account for user id [%s].", commandName, user.getUuid())); diff --git a/plugins/acl/dynamic-role-based/src/test/java/org/apache/cloudstack/acl/DynamicRoleBasedAPIAccessCheckerTest.java b/plugins/acl/dynamic-role-based/src/test/java/org/apache/cloudstack/acl/DynamicRoleBasedAPIAccessCheckerTest.java index 64d264a68e13..93e0f68e7f42 100644 --- a/plugins/acl/dynamic-role-based/src/test/java/org/apache/cloudstack/acl/DynamicRoleBasedAPIAccessCheckerTest.java +++ b/plugins/acl/dynamic-role-based/src/test/java/org/apache/cloudstack/acl/DynamicRoleBasedAPIAccessCheckerTest.java @@ -248,7 +248,7 @@ public void checkAccessTestValidApiKeyPairPermissionWithNullOverride() { Mockito.doReturn(Collections.singletonList(permission)).when(roleServiceMock).findAllPermissionsBy(Mockito.anyLong()); assertTrue(apiAccessCheckerSpy.checkAccess(getTestUser(), api, emptyPermissionArray)); - Mockito.verify(roleServiceMock, Mockito.times(1)).findAllPermissionsBy(Mockito.anyLong()); + Mockito.verify(roleServiceMock, Mockito.times(2)).findAllPermissionsBy(Mockito.anyLong()); } @Test(expected = UnavailableCommandException.class) From 63a9a60093d59d0ecb192feaf99bf6879ebc451f Mon Sep 17 00:00:00 2001 From: Nicole Schmidt Date: Mon, 10 Feb 2025 11:57:58 -0300 Subject: [PATCH 04/17] Fix MySQL syntax error --- .../main/java/com/cloud/upgrade/dao/Upgrade42010to42100.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42010to42100.java b/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42010to42100.java index b80763177c5b..19d0e1b92341 100644 --- a/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42010to42100.java +++ b/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42010to42100.java @@ -91,7 +91,7 @@ private void performKeyPairMigration(Connection conn) throws SQLException { preparedStatement.executeUpdate(); } - pstmt = conn.prepareStatement("ALTER TABLE `cloud`.`user` DROP COLUMN IF EXISTS api_key, DROP COLUMN IF EXISTS secret_key;"); + pstmt = conn.prepareStatement("ALTER TABLE `cloud`.`user` DROP COLUMN api_key, DROP COLUMN secret_key;"); pstmt.executeUpdate(); logger.info("Successfully performed keypair migration."); } catch (SQLException ex) { From 78f2d835f87b9e5d6a5a9ef62a1c04dfa84d1ee2 Mon Sep 17 00:00:00 2001 From: Nicole Schmidt Date: Thu, 13 Feb 2025 11:00:29 -0300 Subject: [PATCH 05/17] Change keypair_permissions table name to api_keypair_permissions --- .../java/org/apache/cloudstack/acl/ApiKeyPairPermissionVO.java | 2 +- .../src/main/resources/META-INF/db/schema-42010to42100.sql | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/engine/schema/src/main/java/org/apache/cloudstack/acl/ApiKeyPairPermissionVO.java b/engine/schema/src/main/java/org/apache/cloudstack/acl/ApiKeyPairPermissionVO.java index 1506f2b47ff4..09f91eb4f2bd 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/acl/ApiKeyPairPermissionVO.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/acl/ApiKeyPairPermissionVO.java @@ -23,7 +23,7 @@ import javax.persistence.Table; @Entity -@Table(name = "keypair_permissions") +@Table(name = "api_keypair_permissions") public class ApiKeyPairPermissionVO extends RolePermissionBaseVO implements ApiKeyPairPermission { @Column(name = "api_keypair_id") private long apiKeyPairId; diff --git a/engine/schema/src/main/resources/META-INF/db/schema-42010to42100.sql b/engine/schema/src/main/resources/META-INF/db/schema-42010to42100.sql index 4d864859acc1..a8f9fa762c52 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-42010to42100.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-42010to42100.sql @@ -103,7 +103,7 @@ CREATE TABLE IF NOT EXISTS `cloud`.`api_keypair` ( CONSTRAINT `fk_api_keypair__domain_id` FOREIGN KEY(`domain_id`) REFERENCES `cloud`.`domain`(`id`) ); -CREATE TABLE IF NOT EXISTS `cloud`.`keypair_permissions` ( +CREATE TABLE IF NOT EXISTS `cloud`.`api_keypair_permissions` ( `id` bigint(20) unsigned NOT NULL auto_increment, `uuid` varchar(40) UNIQUE, `sort_order` bigint(20) unsigned NOT NULL DEFAULT 0, From 0274c67242b15c8460f4b08209d7d210493d5c93 Mon Sep 17 00:00:00 2001 From: Nicole Schmidt Date: Tue, 18 Feb 2025 10:30:09 -0300 Subject: [PATCH 06/17] Remove double imports and fix rebase --- .../main/java/com/cloud/upgrade/dao/Upgrade42010to42100.java | 4 +--- .../java/com/cloud/upgrade/dao/Upgrade42010to42100Test.java | 1 + 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42010to42100.java b/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42010to42100.java index 19d0e1b92341..7a388d086b9c 100644 --- a/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42010to42100.java +++ b/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42010to42100.java @@ -27,8 +27,6 @@ import java.util.UUID; import java.io.InputStream; import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.SQLException; import java.util.List; import org.apache.cloudstack.framework.config.ConfigKey; @@ -62,7 +60,7 @@ public InputStream[] getPrepareScripts() { return new InputStream[] {script}; } - private void performKeyPairMigration(Connection conn) throws SQLException { + protected void performKeyPairMigration(Connection conn) throws SQLException { try { logger.debug("Performing keypair migration from user table to api_keypair table."); PreparedStatement pstmt = conn.prepareStatement("SELECT u.id, u.api_key, u.secret_key, a.domain_id, u.id FROM `cloud`.`user` AS u JOIN `cloud`.`account` AS a " + diff --git a/engine/schema/src/test/java/com/cloud/upgrade/dao/Upgrade42010to42100Test.java b/engine/schema/src/test/java/com/cloud/upgrade/dao/Upgrade42010to42100Test.java index 035790f0716a..470b59e41c1b 100644 --- a/engine/schema/src/test/java/com/cloud/upgrade/dao/Upgrade42010to42100Test.java +++ b/engine/schema/src/test/java/com/cloud/upgrade/dao/Upgrade42010to42100Test.java @@ -47,6 +47,7 @@ public void testPerformDataMigration() throws SQLException { when(dbUpgradeUtils.getTableColumnType(conn, "configuration", "scope")).thenReturn("varchar(255)"); try (MockedStatic ignored2 = Mockito.mockStatic(TransactionLegacy.class)) { + Mockito.doNothing().when(upgrade).performKeyPairMigration(conn); TransactionLegacy txn = Mockito.mock(TransactionLegacy.class); when(TransactionLegacy.currentTxn()).thenReturn(txn); PreparedStatement pstmt = Mockito.mock(PreparedStatement.class); From e737eb47ddd90c5eb95736de06ba1442173ab972 Mon Sep 17 00:00:00 2001 From: Nicole Schmidt Date: Tue, 25 Feb 2025 12:25:36 -0300 Subject: [PATCH 07/17] Fix accessible typo and execute keypair migration through SQL only --- .../admin/user/RegisterUserKeysCmd.java | 4 +- .../apache/cloudstack/query/QueryService.java | 2 +- .../upgrade/dao/Upgrade42010to42100.java | 47 ------------------- .../META-INF/db/schema-42010to42100.sql | 9 ++++ .../upgrade/dao/Upgrade42010to42100Test.java | 1 - .../com/cloud/api/query/QueryManagerImpl.java | 2 +- .../com/cloud/user/AccountManagerImpl.java | 4 +- .../cloudstack/acl/ApiKeyPairManagerImpl.java | 4 +- 8 files changed, 17 insertions(+), 56 deletions(-) diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/user/RegisterUserKeysCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/user/RegisterUserKeysCmd.java index 6d3423570851..b01b95167356 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/user/RegisterUserKeysCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/user/RegisterUserKeysCmd.java @@ -144,8 +144,8 @@ public List> getRules() { @Override public long getEntityOwnerId() { User user = _entityMgr.findById(User.class, getUserId()); - List accessableUsers = _queryService.searchForAccessableUsers(); - if (user != null && accessableUsers.stream().anyMatch(u -> u == user.getId())) { + List accessibleUsers = _queryService.searchForAccessibleUsers(); + if (user != null && accessibleUsers.stream().anyMatch(u -> u == user.getId())) { return user.getAccountId(); } return Account.ACCOUNT_ID_SYSTEM; diff --git a/api/src/main/java/org/apache/cloudstack/query/QueryService.java b/api/src/main/java/org/apache/cloudstack/query/QueryService.java index c915bae04cce..534c896dfe29 100644 --- a/api/src/main/java/org/apache/cloudstack/query/QueryService.java +++ b/api/src/main/java/org/apache/cloudstack/query/QueryService.java @@ -141,7 +141,7 @@ public interface QueryService { ListResponse searchForUsers(Long domainId, boolean recursive) throws PermissionDeniedException; - List searchForAccessableUsers(); + List searchForAccessibleUsers(); ListResponse searchForEvents(ListEventsCmd cmd); diff --git a/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42010to42100.java b/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42010to42100.java index 7a388d086b9c..00a6a6ffb930 100644 --- a/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42010to42100.java +++ b/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42010to42100.java @@ -19,12 +19,8 @@ import com.cloud.upgrade.SystemVmTemplateRegistration; import com.cloud.utils.db.TransactionLegacy; import com.cloud.utils.exception.CloudRuntimeException; -import java.sql.Date; import java.sql.PreparedStatement; -import java.sql.ResultSet; import java.sql.SQLException; -import java.time.LocalDate; -import java.util.UUID; import java.io.InputStream; import java.sql.Connection; import java.util.List; @@ -60,52 +56,9 @@ public InputStream[] getPrepareScripts() { return new InputStream[] {script}; } - protected void performKeyPairMigration(Connection conn) throws SQLException { - try { - logger.debug("Performing keypair migration from user table to api_keypair table."); - PreparedStatement pstmt = conn.prepareStatement("SELECT u.id, u.api_key, u.secret_key, a.domain_id, u.id FROM `cloud`.`user` AS u JOIN `cloud`.`account` AS a " + - "ON u.account_id = a.id WHERE u.api_key IS NOT NULL AND u.secret_key IS NOT NULL"); - ResultSet resultSet = pstmt.executeQuery(); - - while (resultSet.next()) { - long id = resultSet.getLong(1); - String apiKey = resultSet.getString(2); - String secretKey = resultSet.getString(3); - Long domainId = resultSet.getLong(4); - Long accountId = resultSet.getLong(5); - Date timestamp = Date.valueOf(LocalDate.now()); - - PreparedStatement preparedStatement = conn.prepareStatement("INSERT IGNORE INTO `cloud`.`api_keypair` (uuid, user_id, domain_id, account_id, api_key, secret_key, created, name) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"); - String uuid = UUID.randomUUID().toString(); - preparedStatement.setString(1, uuid); - preparedStatement.setLong(2, id); - preparedStatement.setLong(3, domainId); - preparedStatement.setLong(4, accountId); - - preparedStatement.setString(5, apiKey); - preparedStatement.setString(6, secretKey); - preparedStatement.setDate(7, timestamp); - preparedStatement.setString(8, uuid); - - preparedStatement.executeUpdate(); - } - pstmt = conn.prepareStatement("ALTER TABLE `cloud`.`user` DROP COLUMN api_key, DROP COLUMN secret_key;"); - pstmt.executeUpdate(); - logger.info("Successfully performed keypair migration."); - } catch (SQLException ex) { - logger.info("Unexpected exception in user keypair migration", ex); - throw ex; - } - } - @Override public void performDataMigration(Connection conn) { migrateConfigurationScopeToBitmask(conn); - try { - performKeyPairMigration(conn); - } catch (SQLException e) { - throw new RuntimeException(e); - } } @Override diff --git a/engine/schema/src/main/resources/META-INF/db/schema-42010to42100.sql b/engine/schema/src/main/resources/META-INF/db/schema-42010to42100.sql index a8f9fa762c52..5432848885f2 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-42010to42100.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-42010to42100.sql @@ -114,3 +114,12 @@ CREATE TABLE IF NOT EXISTS `cloud`.`api_keypair_permissions` ( PRIMARY KEY (`id`), CONSTRAINT `fk_keypair_permissions__api_keypair_id` FOREIGN KEY(`api_keypair_id`) REFERENCES `cloud`.`api_keypair`(`id`) ); + +INSERT INTO `cloud`.`api_keypair` (uuid, user_id, domain_id, account_id, api_key, secret_key, created, name) +SELECT uuid(), user.id, account.domain_id, account.id, user.api_key, user.secret_key, now(), 'Active key pair' +FROM `cloud`.`user` AS user +JOIN `cloud`.`account` AS account ON user.account_id = account.id +WHERE user.api_key IS NOT NULL + AND user.secret_key IS NOT NULL; + +ALTER TABLE `cloud`.`user` DROP COLUMN IF EXISTS api_key, DROP COLUMN IF EXISTS secret_key; diff --git a/engine/schema/src/test/java/com/cloud/upgrade/dao/Upgrade42010to42100Test.java b/engine/schema/src/test/java/com/cloud/upgrade/dao/Upgrade42010to42100Test.java index 470b59e41c1b..035790f0716a 100644 --- a/engine/schema/src/test/java/com/cloud/upgrade/dao/Upgrade42010to42100Test.java +++ b/engine/schema/src/test/java/com/cloud/upgrade/dao/Upgrade42010to42100Test.java @@ -47,7 +47,6 @@ public void testPerformDataMigration() throws SQLException { when(dbUpgradeUtils.getTableColumnType(conn, "configuration", "scope")).thenReturn("varchar(255)"); try (MockedStatic ignored2 = Mockito.mockStatic(TransactionLegacy.class)) { - Mockito.doNothing().when(upgrade).performKeyPairMigration(conn); TransactionLegacy txn = Mockito.mock(TransactionLegacy.class); when(TransactionLegacy.currentTxn()).thenReturn(txn); PreparedStatement pstmt = Mockito.mock(PreparedStatement.class); 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 08c93d6cce38..6cad044edb6d 100644 --- a/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java +++ b/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java @@ -882,7 +882,7 @@ private Pair, Integer> getUserListInternal(Account calle } @Override - public List searchForAccessableUsers() { + public List searchForAccessibleUsers() { List permittedAccounts = new ArrayList<>(); Account callingAccount = CallContext.current().getCallingAccount(); Filter searchFilter = new Filter(UserAccountJoinVO.class, "id", true); diff --git a/server/src/main/java/com/cloud/user/AccountManagerImpl.java b/server/src/main/java/com/cloud/user/AccountManagerImpl.java index e57fbf646b8e..5d083172ba32 100644 --- a/server/src/main/java/com/cloud/user/AccountManagerImpl.java +++ b/server/src/main/java/com/cloud/user/AccountManagerImpl.java @@ -3184,7 +3184,7 @@ private Integer fetchMultipleKeypairs(List responses, ListUs users = List.of(cmd.getUserId()); } else { User callerUser = CallContext.current().getCallingUser(); - users = cmd.listAll() && isAdmin(callerUser.getAccountId()) ? queryService.searchForAccessableUsers() : List.of(callerUser.getId()); + users = cmd.listAll() && isAdmin(callerUser.getAccountId()) ? queryService.searchForAccessibleUsers() : List.of(callerUser.getId()); } Pair, Integer> keyPairs = apiKeyPairDao.listByUserIdsPaginated(users, cmd); @@ -3287,7 +3287,7 @@ public void validateCallingUserHasAccessToDesiredUser(Long userId) { if (!isAdmin(callerUser.getAccountId()) && callerUser.getId() != userId) { throw new PermissionDeniedException("Only admins can operate on API keys owned by other users"); } - List accessibleUsers = queryService.searchForAccessableUsers(); + List accessibleUsers = queryService.searchForAccessibleUsers(); User desiredUser = _userDao.getUser(userId); if (accessibleUsers.stream().noneMatch(u -> Objects.equals(u, userId))) { throw new PermissionDeniedException(String.format("Could not perform operation because calling user has less permissions " + diff --git a/server/src/main/java/org/apache/cloudstack/acl/ApiKeyPairManagerImpl.java b/server/src/main/java/org/apache/cloudstack/acl/ApiKeyPairManagerImpl.java index 1a45ed8147fc..a263241dccf5 100644 --- a/server/src/main/java/org/apache/cloudstack/acl/ApiKeyPairManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/acl/ApiKeyPairManagerImpl.java @@ -73,9 +73,9 @@ public ApiKeyPair findById(Long id) { @Override public void validateCallingUserHasAccessToDesiredUser(Long userId) { - List accessableUsers = queryService.searchForAccessableUsers(); + List accessibleUsers = queryService.searchForAccessibleUsers(); User desiredUser = userDao.getUser(userId); - if (accessableUsers.stream().noneMatch(u -> Objects.equals(u, userId))) { + if (accessibleUsers.stream().noneMatch(u -> Objects.equals(u, userId))) { throw new InvalidParameterValueException(String.format("Could not perform operation because calling user has less permissions " + "than the informed user [%s].", desiredUser.getId())); } From cd7f0c785543eae46920cec6fb83717000782695 Mon Sep 17 00:00:00 2001 From: Nicole Schmidt Date: Tue, 25 Feb 2025 12:32:10 -0300 Subject: [PATCH 08/17] Change listBy to findOneBy --- .../org/apache/cloudstack/acl/dao/ApiKeyPairDaoImpl.java | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/engine/schema/src/main/java/org/apache/cloudstack/acl/dao/ApiKeyPairDaoImpl.java b/engine/schema/src/main/java/org/apache/cloudstack/acl/dao/ApiKeyPairDaoImpl.java index 6531267a67c8..8d2c8cfa56b2 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/acl/dao/ApiKeyPairDaoImpl.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/acl/dao/ApiKeyPairDaoImpl.java @@ -72,11 +72,7 @@ public ApiKeyPairVO getLastApiKeyCreatedByUser(Long userId) { sc.setParameters("userId", String.valueOf(userId)); } final Filter searchBySorted = new Filter(ApiKeyPairVO.class, "id", false, null, null); - final List apiKeyPairVOList = listBy(sc, searchBySorted); - if (CollectionUtils.isEmpty(apiKeyPairVOList)) { - return null; - } - return apiKeyPairVOList.get(0); + return findOneBy(sc, searchBySorted); } public Pair, Integer> listByUserIdsPaginated(List userIds, ListUserKeysCmd cmd) { From bea13c47b19fb95b7958f069a62d9e32945fc4d1 Mon Sep 17 00:00:00 2001 From: Nicole Schmidt <45316185+nicoschmdt@users.noreply.github.com> Date: Tue, 25 Feb 2025 10:19:56 -0300 Subject: [PATCH 09/17] Apply suggestions from code review Co-authored-by: Fabricio Duarte --- .../org/apache/cloudstack/acl/dao/ApiKeyPairDaoImpl.java | 8 ++------ .../cloudstack/acl/dao/ApiKeyPairPermissionsDaoImpl.java | 2 +- .../main/java/com/cloud/servlet/ConsoleProxyServlet.java | 2 +- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/engine/schema/src/main/java/org/apache/cloudstack/acl/dao/ApiKeyPairDaoImpl.java b/engine/schema/src/main/java/org/apache/cloudstack/acl/dao/ApiKeyPairDaoImpl.java index 8d2c8cfa56b2..2d47238a5409 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/acl/dao/ApiKeyPairDaoImpl.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/acl/dao/ApiKeyPairDaoImpl.java @@ -58,9 +58,7 @@ public ApiKeyPairVO findBySecretKey(String secretKey) { public Pair, Integer> listApiKeysByUserOrApiKeyId(Long userId, Long apiKeyId) { SearchCriteria sc = keyPairSearch.create(); - if (userId != null) { - sc.setParametersIfNotNull("userId", String.valueOf(userId)); - } + sc.setParametersIfNotNull("userId", userId); sc.setParametersIfNotNull("id", apiKeyId); final Filter searchFilter = new Filter(100); return searchAndCount(sc, searchFilter); @@ -68,9 +66,7 @@ public Pair, Integer> listApiKeysByUserOrApiKeyId(Long userId public ApiKeyPairVO getLastApiKeyCreatedByUser(Long userId) { final SearchCriteria sc = keyPairSearch.create(); - if (userId != null) { - sc.setParameters("userId", String.valueOf(userId)); - } + sc.setParametersIfNotNull("userId", userId); final Filter searchBySorted = new Filter(ApiKeyPairVO.class, "id", false, null, null); return findOneBy(sc, searchBySorted); } diff --git a/engine/schema/src/main/java/org/apache/cloudstack/acl/dao/ApiKeyPairPermissionsDaoImpl.java b/engine/schema/src/main/java/org/apache/cloudstack/acl/dao/ApiKeyPairPermissionsDaoImpl.java index d8d12ab6fd2c..0eaf426f983e 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/acl/dao/ApiKeyPairPermissionsDaoImpl.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/acl/dao/ApiKeyPairPermissionsDaoImpl.java @@ -49,7 +49,7 @@ public List findAllByApiKeyPairId(Long apiKeyPairId) { public ApiKeyPairPermissionVO persist(final ApiKeyPairPermissionVO item) { item.setSortOrder(0); final List permissionsList = findAllByKeyPairIdSorted(item.getApiKeyPairId()); - if (permissionsList != null && !permissionsList.isEmpty()) { + if (!CollectionUtils.isEmpty(permissionsList)) { ApiKeyPairPermissionVO lastPermission = permissionsList.get(permissionsList.size() - 1); item.setSortOrder(lastPermission.getSortOrder() + 1); } diff --git a/server/src/main/java/com/cloud/servlet/ConsoleProxyServlet.java b/server/src/main/java/com/cloud/servlet/ConsoleProxyServlet.java index bb8f2b2e4709..5f92064b7a66 100644 --- a/server/src/main/java/com/cloud/servlet/ConsoleProxyServlet.java +++ b/server/src/main/java/com/cloud/servlet/ConsoleProxyServlet.java @@ -555,7 +555,7 @@ private boolean verifyRequest(Map requestParameters) { } if (keyPair == null) { - LOGGER.debug("User does not have a keypair associated with the account -- ignoring request, username: " + user.getUsername()); + LOGGER.debug("User does not have a keypair associated with the account -- ignoring request, username: {}", user.getUsername()); return false; } From 9f5fd28f163d9d9c815ed09dc8614cbe0781e693 Mon Sep 17 00:00:00 2001 From: Nicole Schmidt Date: Wed, 26 Feb 2025 14:18:43 -0300 Subject: [PATCH 10/17] Add missing import --- .../apache/cloudstack/acl/dao/ApiKeyPairPermissionsDaoImpl.java | 1 + 1 file changed, 1 insertion(+) diff --git a/engine/schema/src/main/java/org/apache/cloudstack/acl/dao/ApiKeyPairPermissionsDaoImpl.java b/engine/schema/src/main/java/org/apache/cloudstack/acl/dao/ApiKeyPairPermissionsDaoImpl.java index 0eaf426f983e..8efed169ea5c 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/acl/dao/ApiKeyPairPermissionsDaoImpl.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/acl/dao/ApiKeyPairPermissionsDaoImpl.java @@ -22,6 +22,7 @@ import com.cloud.utils.db.SearchCriteria; import java.util.Collections; import java.util.Objects; +import org.apache.commons.collections.CollectionUtils; import org.apache.cloudstack.acl.ApiKeyPairPermissionVO; import org.springframework.stereotype.Component; From 67fd6e0f011036927403808b4892d90f6a525ee2 Mon Sep 17 00:00:00 2001 From: Nicole Schmidt Date: Thu, 27 Feb 2025 10:12:46 -0300 Subject: [PATCH 11/17] Fix MySQL syntax error --- .../src/main/resources/META-INF/db/schema-42010to42100.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/engine/schema/src/main/resources/META-INF/db/schema-42010to42100.sql b/engine/schema/src/main/resources/META-INF/db/schema-42010to42100.sql index 5432848885f2..c3e3fdbc5196 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-42010to42100.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-42010to42100.sql @@ -122,4 +122,4 @@ JOIN `cloud`.`account` AS account ON user.account_id = account.id WHERE user.api_key IS NOT NULL AND user.secret_key IS NOT NULL; -ALTER TABLE `cloud`.`user` DROP COLUMN IF EXISTS api_key, DROP COLUMN IF EXISTS secret_key; +ALTER TABLE `cloud`.`user` DROP COLUMN api_key, DROP COLUMN secret_key; From c13d06092b435f70ee99ab4a162b2a14fbb5ac12 Mon Sep 17 00:00:00 2001 From: Nicole Schmidt Date: Tue, 13 May 2025 17:48:38 -0300 Subject: [PATCH 12/17] Fix API Keypair permissions to consider changes on role permission --- .../cloudstack/acl/ApiKeyPairManagerImpl.java | 30 +++++++++++++++---- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/server/src/main/java/org/apache/cloudstack/acl/ApiKeyPairManagerImpl.java b/server/src/main/java/org/apache/cloudstack/acl/ApiKeyPairManagerImpl.java index a263241dccf5..b6a65b3dd934 100644 --- a/server/src/main/java/org/apache/cloudstack/acl/ApiKeyPairManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/acl/ApiKeyPairManagerImpl.java @@ -20,13 +20,14 @@ import com.cloud.user.User; import com.cloud.user.dao.UserDao; import com.cloud.utils.component.ManagerBase; + +import java.util.Map; import java.util.stream.Collectors; import org.apache.cloudstack.acl.apikeypair.ApiKeyPairService; import org.apache.cloudstack.acl.apikeypair.ApiKeyPair; import org.apache.cloudstack.acl.apikeypair.ApiKeyPairPermission; import org.apache.cloudstack.acl.dao.ApiKeyPairDao; import org.apache.cloudstack.acl.dao.ApiKeyPairPermissionsDao; -import org.apache.cloudstack.acl.dao.RolePermissionsDao; import org.apache.cloudstack.query.QueryService; import org.apache.commons.collections.CollectionUtils; @@ -44,15 +45,34 @@ public class ApiKeyPairManagerImpl extends ManagerBase implements ApiKeyPairServ @Inject private QueryService queryService; @Inject - private RolePermissionsDao rolePermissionsDao; + private RoleService roleService; @Override public List findAllPermissionsByKeyPairId(Long apiKeyPairId, Long roleId) { List allPermissions = apiKeyPairPermissionsDao.findAllByKeyPairIdSorted(apiKeyPairId); - if (CollectionUtils.isNotEmpty(allPermissions)) { - return allPermissions.stream().map(p -> (ApiKeyPairPermission) p).collect(Collectors.toList()); + List rolePermissionEntity = roleService.findAllRolePermissionsEntityBy(roleId); + + if (!CollectionUtils.isEmpty(allPermissions)) { + List keyPairPermissionsEntity = allPermissions.stream() + .map(p -> (RolePermissionEntity) p).collect(Collectors.toList()); + + Map rolePermissionInfo = roleService.getRoleRulesAndPermissions(rolePermissionEntity); + + if (roleService.roleHasPermission(rolePermissionInfo, keyPairPermissionsEntity)) { + return allPermissions.stream().map(p -> (ApiKeyPairPermission) p).collect(Collectors.toList()); + } + + Map keyPairPermissionInfo = roleService.getRoleRulesAndPermissions(keyPairPermissionsEntity); + if (!roleService.roleHasPermission(keyPairPermissionInfo, rolePermissionEntity)) { + for (RolePermissionEntity rolePermission : keyPairPermissionsEntity) { + if (rolePermission.getPermission() == RolePermissionEntity.Permission.DENY && !rolePermissionEntity.contains(rolePermission)) { + rolePermissionEntity.add(0, rolePermission); + } + } + } } - return rolePermissionsDao.findAllByRoleIdSorted(roleId).stream().map(p -> { + + return rolePermissionEntity.stream().map(p -> { ApiKeyPairPermissionVO permission = new ApiKeyPairPermissionVO(); permission.setRule(p.getRule().getRuleString()); permission.setDescription(p.getDescription()); From d183dd08956e6588a44445a67e55b072f4ba3174 Mon Sep 17 00:00:00 2001 From: Nicole Schmidt Date: Tue, 20 May 2025 14:16:56 -0300 Subject: [PATCH 13/17] fix retrocompatibility issue with getUserKeys --- .../src/main/java/com/cloud/user/AccountManagerImpl.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/server/src/main/java/com/cloud/user/AccountManagerImpl.java b/server/src/main/java/com/cloud/user/AccountManagerImpl.java index 5d083172ba32..fa70e92cb938 100644 --- a/server/src/main/java/com/cloud/user/AccountManagerImpl.java +++ b/server/src/main/java/com/cloud/user/AccountManagerImpl.java @@ -3126,12 +3126,11 @@ public Pair> getKeys(GetUserKeysCmd cmd) { keyPair = _accountService.getLatestUserKeyPair(userId); } - validateKeyPairIsNotNull(keyPair); - validateAccessingKeyPairPermissionsIsSupersetOfAccessedKeyPair(keyPair, cmd); - Map keys = new HashMap<>(); - keys.put("apikey", keyPair.getApiKey()); - keys.put("secretkey", keyPair.getSecretKey()); + if (keyPair != null && isAccessingKeypairSuperset(keyPair, cmd)) { + keys.put("apikey", keyPair.getApiKey()); + keys.put("secretkey", keyPair.getSecretKey()); + } Boolean apiKeyAccess = user.getApiKeyAccess(); if (apiKeyAccess == null) { From 82bc476899ce2278205fe17ea754c6a64f617e6e Mon Sep 17 00:00:00 2001 From: Nicole Schmidt Date: Thu, 22 May 2025 10:18:24 -0300 Subject: [PATCH 14/17] add attributes apikey and secretkey even if values are null --- .../src/main/java/com/cloud/user/AccountManagerImpl.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/server/src/main/java/com/cloud/user/AccountManagerImpl.java b/server/src/main/java/com/cloud/user/AccountManagerImpl.java index fa70e92cb938..89b5096ccfa2 100644 --- a/server/src/main/java/com/cloud/user/AccountManagerImpl.java +++ b/server/src/main/java/com/cloud/user/AccountManagerImpl.java @@ -3127,10 +3127,9 @@ public Pair> getKeys(GetUserKeysCmd cmd) { } Map keys = new HashMap<>(); - if (keyPair != null && isAccessingKeypairSuperset(keyPair, cmd)) { - keys.put("apikey", keyPair.getApiKey()); - keys.put("secretkey", keyPair.getSecretKey()); - } + boolean isAllowed = keyPair != null && isAccessingKeypairSuperset(keyPair, cmd); + keys.put("apikey", isAllowed ? keyPair.getApiKey() : null); + keys.put("secretkey", isAllowed ? keyPair.getSecretKey() : null); Boolean apiKeyAccess = user.getApiKeyAccess(); if (apiKeyAccess == null) { From 86a074a82b84f79ce1e43e632db70df358c9f4bf Mon Sep 17 00:00:00 2001 From: Nicole Schmidt Date: Tue, 27 May 2025 17:21:04 -0300 Subject: [PATCH 15/17] fix access to getUserKeys response --- tools/marvin/marvin/cloudstackTestClient.py | 23 ++++++++++----------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/tools/marvin/marvin/cloudstackTestClient.py b/tools/marvin/marvin/cloudstackTestClient.py index 311abb7d9292..fb2afc19d2e3 100644 --- a/tools/marvin/marvin/cloudstackTestClient.py +++ b/tools/marvin/marvin/cloudstackTestClient.py @@ -148,25 +148,24 @@ def __createApiClient(self): "Client Creation Failed") return FAILED + user_id = list_user_res[0].id getuser_keys = getUserKeys.getUserKeysCmd() - getuser_keys.id = list_user_res[0].id + getuser_keys.id = user_id getuser_keys_res = self.__apiClient.getUserKeys(getuser_keys) - user_id = list_user_res[0].id - if getuser_keys_res is None: + if getuser_keys_res['apikey'] is None: ret = self.__getKeys(user_id) - if ret != FAILED: - mgmt_details.apiKey = ret[0] - mgmt_details.securityKey = ret[1] - else: + if ret == FAILED: self.__logger.error("__createApiClient: API Client " "Creation Failed while " "Registering User") return FAILED + mgmt_details.apiKey = ret[0] + mgmt_details.securityKey = ret[1] else: mgmt_details.port = 8080 - mgmt_details.apiKey = getuser_keys_res[0].apikey - mgmt_details.securityKey = getuser_keys_res[0].secretkey + mgmt_details.apiKey = getuser_keys_res['apikey'] + mgmt_details.securityKey = getuser_keys_res['secretkey'] ''' Now Create the Connection objects and Api Client using new details @@ -220,11 +219,11 @@ def __getKeys(self, userid): getuser_keys_res = self.__apiClient.getUserKeys(getuser_keys) if getuser_keys_res is None: self.__logger.error("__createApiClient: API " - "Client Creation Failed") + "Client Creation Failed") return FAILED - api_key = getuser_keys_res[0].apikey - security_key = getuser_keys_res[0].secretkey + api_key = getuser_keys_res['apikey'] + security_key = getuser_keys_res['secretkey'] return (api_key, security_key) except Exception as e: self.__logger.exception("Exception Occurred Under __geKeys : " From e2f9ee84e7193cdf175aaeab33bc565138a6e93a Mon Sep 17 00:00:00 2001 From: Nicole Schmidt Date: Tue, 27 May 2025 17:47:39 -0300 Subject: [PATCH 16/17] remove duplicated import --- server/src/test/java/com/cloud/user/AccountManagerImplTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/server/src/test/java/com/cloud/user/AccountManagerImplTest.java b/server/src/test/java/com/cloud/user/AccountManagerImplTest.java index d086ce1970c4..bfad7135dc76 100644 --- a/server/src/test/java/com/cloud/user/AccountManagerImplTest.java +++ b/server/src/test/java/com/cloud/user/AccountManagerImplTest.java @@ -65,7 +65,6 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import static org.mockito.ArgumentMatchers.any; import org.mockito.ArgumentCaptor; import org.mockito.InOrder; import org.mockito.Mock; From 493852d012cb16c2bcafa4d6fc6abe1b65dd7bdb Mon Sep 17 00:00:00 2001 From: Nicole Schmidt Date: Fri, 30 May 2025 13:15:48 -0300 Subject: [PATCH 17/17] add missing mock in keypair tests --- .../test/java/com/cloud/user/AccountManagerImplTest.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/server/src/test/java/com/cloud/user/AccountManagerImplTest.java b/server/src/test/java/com/cloud/user/AccountManagerImplTest.java index bfad7135dc76..3ff18fd854df 100644 --- a/server/src/test/java/com/cloud/user/AccountManagerImplTest.java +++ b/server/src/test/java/com/cloud/user/AccountManagerImplTest.java @@ -1293,6 +1293,7 @@ public void createApiKeyAndSecretKeyTestWithEmptyRules() { Mockito.when(userDaoMock.findById(any())).thenReturn(userVoMock); Mockito.when(_accountDao.findById(Mockito.anyLong())).thenReturn(accountVoMock); Mockito.doNothing().when(accountManagerImpl).checkAccess(Mockito.any(Account.class), Mockito.isNull(), Mockito.anyBoolean(), Mockito.any(Account.class)); + Mockito.doNothing().when(accountManagerImpl).verifyCallerPrivilegeForUserOrAccountOperations(Mockito.any(User.class)); Mockito.when(apiKeyPairDaoMock.findBySecretKey(Mockito.anyString())).thenReturn(null); Mockito.when(roleService.findAllPermissionsBy(Mockito.anyLong())).thenReturn(List.of( new RolePermissionVO(1L, "api2", RolePermissionEntity.Permission.ALLOW, "description") @@ -1326,6 +1327,7 @@ public void createApiKeyAndSecretKeyTestPermissionNotPresentOnAccount() { Mockito.when(userDaoMock.findById(any())).thenReturn(userVoMock); Mockito.when(_accountDao.findById(Mockito.anyLong())).thenReturn(accountVoMock); Mockito.doNothing().when(accountManagerImpl).checkAccess(Mockito.any(Account.class), Mockito.isNull(), Mockito.anyBoolean(), Mockito.any()); + Mockito.doNothing().when(accountManagerImpl).verifyCallerPrivilegeForUserOrAccountOperations(Mockito.any(User.class)); Mockito.when(roleService.findAllPermissionsBy(Mockito.anyLong())).thenReturn(List.of( new RolePermissionVO(1L, "api2", RolePermissionEntity.Permission.ALLOW, "description") )); @@ -1358,6 +1360,7 @@ public void createApiKeyAndSecretTestKeyDeniedOnAccount() { Mockito.when(userDaoMock.findById(Mockito.anyLong())).thenReturn(userVoMock); Mockito.when(_accountDao.findById(Mockito.anyLong())).thenReturn(accountVoMock); Mockito.doNothing().when(accountManagerImpl).checkAccess(Mockito.any(Account.class), Mockito.isNull(), Mockito.anyBoolean(), Mockito.any()); + Mockito.doNothing().when(accountManagerImpl).verifyCallerPrivilegeForUserOrAccountOperations(Mockito.any(User.class)); Mockito.when(apiKeyPairDaoMock.findBySecretKey(Mockito.anyString())).thenReturn(null); Mockito.when(roleService.findAllPermissionsBy(Mockito.anyLong())).thenReturn(List.of( new RolePermissionVO(1L, "api", RolePermissionEntity.Permission.DENY, "description") @@ -1392,6 +1395,7 @@ public void createApiKeyAndSecretKeyTestAllowedOnAccount() { Mockito.when(userDaoMock.findById(Mockito.anyLong())).thenReturn(userVoMock); Mockito.when(_accountDao.findById(Mockito.anyLong())).thenReturn(accountVoMock); Mockito.doNothing().when(accountManagerImpl).checkAccess(Mockito.any(Account.class), Mockito.isNull(), Mockito.anyBoolean(), Mockito.any(Account.class)); + Mockito.doNothing().when(accountManagerImpl).verifyCallerPrivilegeForUserOrAccountOperations(Mockito.any(User.class)); Mockito.when(apiKeyPairPermissionsDaoMock.persist(Mockito.any(ApiKeyPairPermissionVO.class))).thenReturn(permissionVO); Mockito.doReturn(true).when(roleService).roleHasPermission(Mockito.any(), Mockito.any()); Mockito.when(roleService.findRole(Mockito.anyLong())).thenReturn(new RoleVO()); @@ -1430,6 +1434,7 @@ public void createApiAndSecretKeyTestWithNonEmptyDates() { Mockito.when(userDaoMock.findById(Mockito.anyLong())).thenReturn(userVoMock); Mockito.when(_accountDao.findById(Mockito.anyLong())).thenReturn(accountVoMock); Mockito.doNothing().when(accountManagerImpl).checkAccess(Mockito.any(Account.class), Mockito.isNull(), Mockito.anyBoolean(), Mockito.any(Account.class)); + Mockito.doNothing().when(accountManagerImpl).verifyCallerPrivilegeForUserOrAccountOperations(Mockito.any(User.class)); Mockito.when(apiKeyPairPermissionsDaoMock.persist(Mockito.any(ApiKeyPairPermissionVO.class))).thenReturn(permissionVO); Mockito.when(apiKeyPairDaoMock.findBySecretKey(Mockito.anyString())).thenReturn(null); Mockito.doReturn(true).when(roleService).roleHasPermission(Mockito.any(), Mockito.any()); @@ -1473,6 +1478,7 @@ public void createApiAndSecretKeyTestWithExpiredDate() { Mockito.when(userDaoMock.findById(Mockito.anyLong())).thenReturn(userVoMock); Mockito.when(_accountDao.findById(Mockito.anyLong())).thenReturn(accountVoMock); Mockito.doNothing().when(accountManagerImpl).checkAccess(Mockito.any(Account.class), Mockito.isNull(), Mockito.anyBoolean(), Mockito.any(Account.class)); + Mockito.doNothing().when(accountManagerImpl).verifyCallerPrivilegeForUserOrAccountOperations(Mockito.any(User.class)); ApiKeyPair response = accountManagerImpl.createApiKeyAndSecretKey(registerCmdMock); Assert.assertEquals((long) response.getUserId(), userId); @@ -1511,6 +1517,7 @@ public void createApiAndSecretKeyTestWithInvalidDate() { Mockito.when(userDaoMock.findById(Mockito.anyLong())).thenReturn(userVoMock); Mockito.when(_accountDao.findById(Mockito.anyLong())).thenReturn(accountVoMock); Mockito.doNothing().when(accountManagerImpl).checkAccess(Mockito.any(Account.class), Mockito.isNull(), Mockito.anyBoolean(), Mockito.any(Account.class)); + Mockito.doNothing().when(accountManagerImpl).verifyCallerPrivilegeForUserOrAccountOperations(Mockito.any(User.class)); ApiKeyPair response = accountManagerImpl.createApiKeyAndSecretKey(registerCmdMock); Assert.assertEquals(userId, (long) response.getUserId()); @@ -1552,6 +1559,7 @@ public void createApiAndSecretKeyTestWithMultipleAllowedPermissionsOneDenied() { Mockito.when(userDaoMock.findById(Mockito.anyLong())).thenReturn(userVoMock); Mockito.when(_accountDao.findById(Mockito.anyLong())).thenReturn(accountVoMock); Mockito.doNothing().when(accountManagerImpl).checkAccess(Mockito.any(Account.class), Mockito.isNull(), Mockito.anyBoolean(), Mockito.any(Account.class)); + Mockito.doNothing().when(accountManagerImpl).verifyCallerPrivilegeForUserOrAccountOperations(Mockito.any(User.class)); Mockito.when(apiKeyPairDaoMock.findBySecretKey(Mockito.anyString())).thenReturn(null); Mockito.when(roleService.findAllPermissionsBy(Mockito.anyLong())).thenReturn(List.of( new RolePermissionVO(1L, "api1", RolePermissionEntity.Permission.ALLOW, "description-1"), @@ -1597,6 +1605,7 @@ public void createApiAndSecretKeyTestWithMultipleAllowedPermissionsOneDoesNotExi Mockito.when(userDaoMock.findById(Mockito.anyLong())).thenReturn(userVoMock); Mockito.when(_accountDao.findById(Mockito.anyLong())).thenReturn(accountVoMock); Mockito.doNothing().when(accountManagerImpl).checkAccess(Mockito.any(Account.class), Mockito.isNull(), Mockito.anyBoolean(), Mockito.any(Account.class)); + Mockito.doNothing().when(accountManagerImpl).verifyCallerPrivilegeForUserOrAccountOperations(Mockito.any(User.class)); Mockito.when(apiKeyPairDaoMock.findBySecretKey(Mockito.anyString())).thenReturn(null); Mockito.when(roleService.findAllPermissionsBy(Mockito.anyLong())).thenReturn(List.of( new RolePermissionVO(1L, "api1", RolePermissionEntity.Permission.ALLOW, "description-1"),