From 12576789aa9efdafc7cdbcee3ebb289ac3d9c7a3 Mon Sep 17 00:00:00 2001 From: "klaus.freitas.scclouds" Date: Tue, 30 Jul 2024 14:13:09 -0300 Subject: [PATCH 01/22] initial commit of api keypair restrucure --- .../main/java/com/cloud/event/EventTypes.java | 3 +- .../java/com/cloud/user/AccountService.java | 14 +- api/src/main/java/com/cloud/user/User.java | 8 - .../org/apache/cloudstack/acl/APIChecker.java | 6 +- .../apache/cloudstack/acl/RoleService.java | 2 + .../cloudstack/acl/apikeypair/ApiKeyPair.java | 36 ++ .../acl/apikeypair/ApiKeyPairPermission.java | 23 + .../acl/apikeypair/ApiKeyPairService.java | 31 ++ .../apache/cloudstack/api/ApiConstants.java | 1 + .../org/apache/cloudstack/api/BaseCmd.java | 3 + .../cloudstack/api/ResponseGenerator.java | 9 + .../command/admin/user/DeleteUserKeysCmd.java | 86 ++++ .../admin/user/ListUserKeyRulesCmd.java | 76 +++ .../command/admin/user/ListUserKeysCmd.java | 86 ++++ .../api/command/admin/user/RegisterCmd.java | 149 +++++- .../api/response/ApiKeyPairResponse.java | 245 +++++++++ .../apache/cloudstack/query/QueryService.java | 2 + .../upgrade/dao/Upgrade41910to42000.java | 54 ++ .../java/com/cloud/user/UserAccountVO.java | 25 - .../src/main/java/com/cloud/user/UserVO.java | 30 -- .../java/com/cloud/user/dao/AccountDao.java | 4 +- .../com/cloud/user/dao/AccountDaoImpl.java | 50 +- .../com/cloud/user/dao/UserAccountDao.java | 2 - .../cloud/user/dao/UserAccountDaoImpl.java | 16 - .../main/java/com/cloud/user/dao/UserDao.java | 7 - .../java/com/cloud/user/dao/UserDaoImpl.java | 12 - .../acl/ApiKeyPairPermissionVO.java | 55 ++ .../apache/cloudstack/acl/ApiKeyPairVO.java | 244 +++++++++ .../cloudstack/acl/dao/ApiKeyPairDao.java | 39 ++ .../cloudstack/acl/dao/ApiKeyPairDaoImpl.java | 118 +++++ .../acl/dao/ApiKeyPairPermissionsDao.java | 28 + .../acl/dao/ApiKeyPairPermissionsDaoImpl.java | 67 +++ ...spring-engine-schema-core-daos-context.xml | 2 + .../META-INF/db/schema-41910to42000.sql | 34 ++ .../META-INF/db/views/cloud.user_view.sql | 2 - .../acl/DynamicRoleBasedAPIAccessChecker.java | 40 +- .../DynamicRoleBasedAPIAccessCheckerTest.java | 67 +++ .../acl/ProjectRoleBasedApiAccessChecker.java | 5 +- .../acl/StaticRoleBasedAPIAccessChecker.java | 5 +- .../ratelimit/ApiRateLimitServiceImpl.java | 7 +- .../manager/BaremetalVlanManagerImpl.java | 5 +- .../cluster/KubernetesClusterManagerImpl.java | 6 +- .../tungsten/service/TungstenServiceImpl.java | 7 +- .../main/java/com/cloud/api/ApiDBUtils.java | 10 + .../java/com/cloud/api/ApiResponseHelper.java | 60 +++ .../main/java/com/cloud/api/ApiServer.java | 55 +- .../com/cloud/api/query/QueryManagerImpl.java | 23 + .../api/query/dao/UserAccountJoinDaoImpl.java | 8 +- .../cloud/api/query/vo/UserAccountJoinVO.java | 15 - .../network/as/AutoScaleManagerImpl.java | 6 +- .../lb/LoadBalancingRulesManagerImpl.java | 7 +- .../VirtualNetworkApplianceManagerImpl.java | 7 +- .../cloud/server/ManagementServerImpl.java | 10 +- .../cloud/servlet/ConsoleProxyServlet.java | 16 +- .../java/com/cloud/user/AccountManager.java | 3 +- .../com/cloud/user/AccountManagerImpl.java | 322 ++++++++++-- .../cloudstack/acl/ApiKeyPairManagerImpl.java | 104 ++++ .../cloudstack/acl/RoleManagerImpl.java | 3 +- .../cloud/user/AccountManagerImplTest.java | 483 +++++++++++++++++- .../cloud/user/MockAccountManagerImpl.java | 20 +- .../acl/ApiKeyPairManagerImplTest.java | 56 ++ 61 files changed, 2617 insertions(+), 302 deletions(-) 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 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 create mode 100644 server/src/test/java/org/apache/cloudstack/acl/ApiKeyPairManagerImplTest.java diff --git a/api/src/main/java/com/cloud/event/EventTypes.java b/api/src/main/java/com/cloud/event/EventTypes.java index d4235cbc9bcb..5b718aae0835 100644 --- a/api/src/main/java/com/cloud/event/EventTypes.java +++ b/api/src/main/java/com/cloud/event/EventTypes.java @@ -284,8 +284,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"; // Template Events public static final String EVENT_TEMPLATE_CREATE = "TEMPLATE.CREATE"; diff --git a/api/src/main/java/com/cloud/user/AccountService.java b/api/src/main/java/com/cloud/user/AccountService.java index 63f5455cfd09..d6d4faceed8a 100644 --- a/api/src/main/java/com/cloud/user/AccountService.java +++ b/api/src/main/java/com/cloud/user/AccountService.java @@ -22,8 +22,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.api.command.admin.account.CreateAccountCmd; 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.RegisterCmd; import org.apache.cloudstack.api.command.admin.user.UpdateUserCmd; @@ -34,6 +36,8 @@ import com.cloud.offering.DiskOffering; import com.cloud.offering.NetworkOffering; import com.cloud.offering.ServiceOffering; +import org.apache.cloudstack.api.response.ApiKeyPairResponse; +import org.apache.cloudstack.api.response.ListResponse; import org.apache.cloudstack.auth.UserTwoFactorAuthenticator; public interface AccountService { @@ -92,7 +96,7 @@ User createUser(String userName, String password, String firstName, String lastN void markUserRegistered(long userId); - public String[] createApiKeyAndSecretKey(RegisterCmd cmd); + public ApiKeyPair createApiKeyAndSecretKey(RegisterCmd cmd); public String[] createApiKeyAndSecretKey(final long userId); @@ -125,9 +129,9 @@ User createUser(String userName, String password, String firstName, String lastN */ UserAccount getUserAccountById(Long userId); - public Map getKeys(GetUserKeysCmd cmd); + Map getKeys(GetUserKeysCmd cmd); - public Map getKeys(Long userId); + ListResponse getKeys(ListUserKeysCmd cmd); /** * Lists user two-factor authentication provider plugins @@ -142,4 +146,8 @@ User createUser(String userName, String password, String firstName, String lastN */ UserTwoFactorAuthenticator getUserTwoFactorAuthenticationProvider(final Long domainId); + ApiKeyPair getLatestUserKeyPair(Long userId); + + ApiKeyPair getKeyPairById(Long id); + } diff --git a/api/src/main/java/com/cloud/user/User.java b/api/src/main/java/com/cloud/user/User.java index 422e264f10be..7029e8b304e3 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/org/apache/cloudstack/acl/APIChecker.java b/api/src/main/java/org/apache/cloudstack/acl/APIChecker.java index 660f64f43ef2..f7d29a91b789 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,9 @@ 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 07f62a7e7f8f..d0baac960188 100644 --- a/api/src/main/java/org/apache/cloudstack/acl/RoleService.java +++ b/api/src/main/java/org/apache/cloudstack/acl/RoleService.java @@ -96,4 +96,6 @@ public interface RoleService { List findAllPermissionsBy(Long roleId); Permission getRolePermission(String permission); + + int removeRolesIfNeeded(List roles); } 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..6a97b11e8000 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/acl/apikeypair/ApiKeyPair.java @@ -0,0 +1,36 @@ +// 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(); + boolean validateDate(boolean throwException); +} 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..7e3da6d4e681 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/acl/apikeypair/ApiKeyPairService.java @@ -0,0 +1,31 @@ +// 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); + + ApiKeyPair findByApiKey(String apiKey); + + ApiKeyPair findById(Long id); + + void deleteApiKey(ApiKeyPair 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 7a167a7aeb60..981399fccb62 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -288,6 +288,7 @@ public class ApiConstants { public static final String KEEPALIVE_ENABLED = "keepaliveenabled"; public static final String KERNEL_VERSION = "kernelversion"; public static final String KEY = "key"; + public static final String KEYPAIR_ID = "keypairid"; public static final String LABEL = "label"; public static final String LASTNAME = "lastname"; public static final String LAST_BOOT = "lastboottime"; 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 b206cd011c1d..c71740d8cc30 100644 --- a/api/src/main/java/org/apache/cloudstack/api/BaseCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/BaseCmd.java @@ -34,6 +34,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; @@ -217,6 +218,8 @@ public static enum CommandType { public VnfTemplateManager vnfTemplateManager; @Inject public BucketApiService _bucketService; + @Inject + public ApiKeyPairService apiKeyPairService; public abstract void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException, 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 ef759aaf9c3e..d9671e5b68b0 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ResponseGenerator.java +++ b/api/src/main/java/org/apache/cloudstack/api/ResponseGenerator.java @@ -22,6 +22,10 @@ import java.util.Map; import java.util.Set; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPair; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPairPermission; +import org.apache.cloudstack.api.response.ApiKeyPairResponse; +import org.apache.cloudstack.api.response.BaseRolePermissionResponse; import org.apache.cloudstack.storage.object.Bucket; import org.apache.cloudstack.affinity.AffinityGroup; import org.apache.cloudstack.affinity.AffinityGroupResponse; @@ -549,4 +553,9 @@ List createTemplateResponses(ResponseView view, VirtualMachine ObjectStoreResponse createObjectStoreResponse(ObjectStore os); BucketResponse createBucketResponse(Bucket bucket); + + ApiKeyPairResponse createKeyPairResponse(ApiKeyPair keyPair); + + ListResponse createKeypairPermissionsResponse(List permissions); + } 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..b0f19341b708 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/user/DeleteUserKeysCmd.java @@ -0,0 +1,86 @@ +// 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.exception.InvalidParameterValueException; +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.18.0.11-scclouds", requestHasSensitiveInfo = false, responseHasSensitiveInfo = false) +public class DeleteUserKeysCmd extends BaseAsyncCmd { + + @ACL + @Parameter(name = ApiConstants.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; + } + + private Long getId() { + return id; + } + + @Override + public void execute() { + ApiKeyPair keyPair = apiKeyPairService.findById(getId()); + if (keyPair == null) { + throw new InvalidParameterValueException(String.format("No keypair found with the id [%s].", getId())); + } + apiKeyPairService.validateCallingUserHasAccessToDesiredUser(keyPair.getUserId()); + + apiKeyPairService.deleteApiKey(keyPair); + + 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/ListUserKeyRulesCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/user/ListUserKeyRulesCmd.java new file mode 100644 index 000000000000..2b111264de8d --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/user/ListUserKeyRulesCmd.java @@ -0,0 +1,76 @@ +// 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.InvalidParameterValueException; +import com.cloud.exception.ResourceUnavailableException; +import com.cloud.user.Account; +import java.util.List; +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.BaseCmd; +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; + +@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.18.0.11-scclouds") + +public class ListUserKeyRulesCmd extends BaseCmd { + + @ACL + @Parameter(name=ApiConstants.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 { + ApiKeyPair keyPair = apiKeyPairService.findById(getId()); + if (keyPair == null) { + throw new InvalidParameterValueException(String.format("No keypair found with the id [%s].", getId())); + } + apiKeyPairService.validateCallingUserHasAccessToDesiredUser(keyPair.getUserId()); + + List permissions = apiKeyPairService.findAllPermissionsByKeyPairId(keyPair.getId()); + 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..0790f6418ee2 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/user/ListUserKeysCmd.java @@ -0,0 +1,86 @@ +// 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; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +@APICommand(name = "listUserKeys", + description = "This command allows the user to list the API Key pairs (api and secret keys) for a user", + responseObject = ApiKeyPairResponse.class, + requestHasSensitiveInfo = false, + responseHasSensitiveInfo = true, + authorized = {RoleType.User, RoleType.Admin, RoleType.DomainAdmin, RoleType.ResourceAdmin}, + since = "4.10.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; + + protected Logger logger = LogManager.getLogger(getClass()); + + public Long getUserId() { + return userId; + } + + public Long getKeyId() { + return keyPairId; + } + + 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.getKeys(this); + finalResponse.setObjectName("userkeys"); + finalResponse.setResponseName(getCommandName()); + setResponseObject(finalResponse); + } +} \ No newline at end of file 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 index b3e7d2bec821..54801f748df5 100644 --- 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 @@ -16,61 +16,153 @@ // under the License. package org.apache.cloudstack.api.command.admin.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.BaseCmd; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.context.CallContext; +import org.apache.commons.lang3.StringUtils; 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.ApiKeyPairResponse; import org.apache.cloudstack.api.response.UserResponse; -import com.cloud.user.Account; import com.cloud.user.User; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; @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) + 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 RegisterCmd extends BaseCmd { + protected Logger logger = LogManager.getLogger(getClass()); + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = UserResponse.class, required = true, description = "User ID.") + private Long id; - ///////////////////////////////////////////////////// - //////////////// API parameters ///////////////////// - ///////////////////////////////////////////////////// + @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, description = "API keypair name.") + private String name; - @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = UserResponse.class, required = true, description = "User id") - private Long id; + @Parameter(name = ApiConstants.DESCRIPTION, type = CommandType.STRING, description = "API keypair description.") + private String description; - ///////////////////////////////////////////////////// - /////////////////// Accessors /////////////////////// - ///////////////////////////////////////////////////// + @Parameter(name = ApiConstants.START_DATE, type = CommandType.DATE, description = "Start date for the API keypair.") + private Date startDate; - public Long getId() { - return id; + @Parameter(name = ApiConstants.END_DATE, type = CommandType.DATE, description = "Expiration date for the API keypair.") + 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 void setId(Long id) { - this.id = id; + 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; + } + + Collection rulesCollection = rules.values(); + for (Object ruleObject : rulesCollection) { + 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, getId()); - if (user != null) { - return user.getAccountId(); + if (id != null) { + return id; } + return CallContext.current().getCallingAccount().getId(); + } - return Account.ACCOUNT_ID_SYSTEM; // no account info given, parent this command to SYSTEM so ERROR events are tracked + public Long getUserId() { + return id; } @Override public Long getApiResourceId() { - return id; + User user = _entityMgr.findById(User.class, getUserId()); + if (user != null) { + return user.getId(); + } + return null; } @Override @@ -80,11 +172,12 @@ public ApiCommandResourceType getApiResourceType() { @Override public void execute() { - String[] keys = _accountService.createApiKeyAndSecretKey(this); - RegisterResponse response = new RegisterResponse(); - if (keys != null) { - response.setApiKey(keys[0]); - response.setSecretKey(keys[1]); + apiKeyPairService.validateCallingUserHasAccessToDesiredUser(id); + + ApiKeyPair apiKeyPair = _accountService.createApiKeyAndSecretKey(this); + ApiKeyPairResponse response = new ApiKeyPairResponse(); + if (apiKeyPair != null) { + response = _responseGenerator.createKeyPairResponse(apiKeyPair); } response.setObjectName("userkeys"); response.setResponseName(getCommandName()); 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..461be1fc86ea --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/response/ApiKeyPairResponse.java @@ -0,0 +1,245 @@ +// 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.serializer.Param; +import com.google.gson.annotations.SerializedName; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPair; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseResponseWithAnnotations; +import org.apache.cloudstack.api.EntityReference; + +import java.util.Date; + +@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 = "User name of the keypair's owner.") + private String userName; + + @SerializedName(ApiConstants.UUID) + @Param(description = "UUID of the API keypair.", isSensitive = true) + private String uuid; + + @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 (admin, domain-admin, user).") + private Integer accountType; + + @SerializedName(ApiConstants.ROLE_ID) + @Param(description = "ID of the role.") + private Long 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.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 String 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 getUuid() { + return this.uuid; + } + + public void setUuid(String uuid) { + this.uuid = uuid; + } + + 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 Integer getAccountType() { + return accountType; + } + + public void setAccountType(Integer accountType) { + this.accountType = accountType; + } + + public Long getRoleId() { + return roleId; + } + + public void setRoleId(Long roleId) { + this.roleId = roleId; + } + + 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 String getState() { + return state; + } + + public void setState(String state) { + this.state = state; + } +} 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 c93e43d9f371..9ae17de87d74 100644 --- a/api/src/main/java/org/apache/cloudstack/query/QueryService.java +++ b/api/src/main/java/org/apache/cloudstack/query/QueryService.java @@ -130,6 +130,8 @@ public interface QueryService { ConfigKey ReturnVmStatsOnVmList = new ConfigKey<>("Advanced", Boolean.class, "list.vm.default.details.stats", "true", "Determines whether VM stats should be returned when details are not explicitly specified in listVirtualMachines API request. When false, details default to [group, nics, secgrp, tmpl, servoff, diskoff, backoff, iso, volume, min, affgrp]. When true, all details are returned including 'stats'.", true, ConfigKey.Scope.Global); + List searchForAccessableUsers(); + ListResponse searchForUsers(ListUsersCmd cmd) throws PermissionDeniedException; ListResponse searchForUsers(Long domainId, boolean recursive) throws PermissionDeniedException; diff --git a/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade41910to42000.java b/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade41910to42000.java index d8c7684f8e3a..175693e4b0d6 100644 --- a/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade41910to42000.java +++ b/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade41910to42000.java @@ -18,11 +18,21 @@ import java.io.InputStream; import java.sql.Connection; +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 com.cloud.upgrade.SystemVmTemplateRegistration; import com.cloud.utils.exception.CloudRuntimeException; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; public class Upgrade41910to42000 extends DbUpgradeAbstractImpl implements DbUpgrade, DbUpgradeSystemVmTemplate { + protected Logger logger = LogManager.getLogger(Upgrade41910to42000.class); + private SystemVmTemplateRegistration systemVmTemplateRegistration; @Override @@ -51,8 +61,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) { + 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 c18ca53f7abe..1673f714eeaf 100644 --- a/engine/schema/src/main/java/com/cloud/user/UserAccountVO.java +++ b/engine/schema/src/main/java/com/cloud/user/UserAccountVO.java @@ -67,13 +67,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; @@ -201,24 +194,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 69970bf2d2cd..a45f74bce69a 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; @@ -147,8 +139,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()); @@ -240,26 +230,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 17b07496731e..87e9298da107 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 @@ -20,14 +20,16 @@ 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; import java.util.Date; import java.util.List; 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 eed5572a0b24..1c6927424026 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 @@ -22,6 +22,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; @@ -30,6 +31,8 @@ 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 org.apache.commons.lang3.StringUtils; import com.cloud.utils.db.TransactionLegacy; import org.springframework.stereotype.Component; @@ -41,9 +44,11 @@ @Component public class AccountDaoImpl extends GenericDaoBase implements AccountDao { - private static final String FIND_USER_ACCOUNT_BY_API_KEY = "SELECT u.id, u.username, u.account_id, u.secret_key, u.state, " - + "a.id, a.account_name, a.type, a.role_id, a.domain_id, a.state " + "FROM `cloud`.`user` u, `cloud`.`account` a " - + "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, " + + "a.id, a.account_name, a.type, a.role_id, a.domain_id, a.state, 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; @@ -132,10 +137,10 @@ public List findCleanupsForDisabledAccounts() { } @Override - public Pair findUserAccountByApiKey(String apiKey) { + public Ternary findUserAccountByApiKey(String apiKey) { TransactionLegacy txn = TransactionLegacy.currentTxn(); PreparedStatement pstmt = null; - Pair userAcctPair = null; + Ternary userAcctTernary = null; try { String sql = FIND_USER_ACCOUNT_BY_API_KEY; pstmt = txn.prepareAutoCloseStatement(sql); @@ -143,25 +148,30 @@ public Pair findUserAccountByApiKey(String 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))); - - AccountVO a = new AccountVO(rs.getLong(6)); - a.setAccountName(rs.getString(7)); - a.setType(Account.Type.getFromValue(rs.getInt(8))); - a.setRoleId(rs.getLong(9)); - a.setDomainId(rs.getLong(10)); - a.setState(State.getValueOf(rs.getString(11))); - - userAcctPair = new Pair(u, a); + User user = new UserVO(rs.getLong(1)); + user.setUsername(rs.getString(2)); + user.setAccountId(rs.getLong(3)); + user.setState(State.getValueOf(rs.getString(4))); + + AccountVO account = new AccountVO(rs.getLong(5)); + account.setAccountName(rs.getString(6)); + account.setType(Account.Type.getFromValue(rs.getInt(7))); + account.setRoleId(rs.getLong(8)); + account.setDomainId(rs.getLong(9)); + account.setState(State.getValueOf(rs.getString(10))); + + ApiKeyPairVO keyPair = new ApiKeyPairVO(rs.getLong(11)); + keyPair.setStartDate(rs.getDate(12)); + keyPair.setEndDate(rs.getDate(13)); + keyPair.setSecretKey(DBEncryptionUtil.decrypt(rs.getString(14))); + keyPair.setRemoved(rs.getDate(15)); + + userAcctTernary = new Ternary(user, account, keyPair); } } catch (Exception 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..e0d3328e7377 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 @@ -28,14 +28,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 +71,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..c823796dce57 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 @@ -37,7 +37,6 @@ public class UserDaoImpl extends GenericDaoBase implements UserDao protected SearchBuilder UsernameLikeSearch; protected SearchBuilder UserIdSearch; protected SearchBuilder AccountIdSearch; - protected SearchBuilder SecretKeySearch; protected SearchBuilder RegistrationTokenSearch; @Inject @@ -65,10 +64,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 +116,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..58121ed0847e --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/acl/ApiKeyPairPermissionVO.java @@ -0,0 +1,55 @@ +// 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.RolePermissionBaseVO; +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 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..2b65982b074a --- /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 org.apache.cloudstack.acl.apikeypair.ApiKeyPair; +import org.joda.time.DateTime; + +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.time.Instant; +import java.util.Date; +import java.util.UUID; + +@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 = name; + 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 boolean validateDate(boolean throwException) throws PermissionDeniedException { + Date now = DateTime.now().toDate(); + Date keypairStart = this.getStartDate(); + Date keypairExpiration = this.getEndDate(); + if (keypairStart != null && now.compareTo(keypairStart) <= 0) { + if (throwException) { + throw new PermissionDeniedException(String.format("Keypair is not valid yet, start date: %s", keypairStart)); + } + return false; + } + if (keypairExpiration != null && now.compareTo(keypairExpiration) >= 0) { + if (throwException) { + throw new PermissionDeniedException(String.format("Keypair is expired, expiration date: %s", keypairExpiration)); + } + return false; + } + return true; + } + + 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..0fb72d920c82 --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/acl/dao/ApiKeyPairDao.java @@ -0,0 +1,39 @@ +// 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.GenericDao; +import org.apache.cloudstack.acl.ApiKeyPairVO; +import org.apache.cloudstack.api.command.admin.user.ListUserKeysCmd; + +import java.util.List; + +public interface ApiKeyPairDao extends GenericDao { + ApiKeyPairVO findBySecretKey(String secretKey); + + ApiKeyPairVO findByApiKey(String apiKey); + + ApiKeyPairVO findByUuid(String uuid); + + Pair, Integer> listApiKeysByUserOrId(Long userId, Long id); + + 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..ec14009383d3 --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/acl/dao/ApiKeyPairDaoImpl.java @@ -0,0 +1,118 @@ +// 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 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; + +import java.util.List; + +@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> listApiKeysByUserOrId(Long userId, Long id) { + SearchCriteria sc = keyPairSearch.create(); + if (userId != null) { + sc.setParametersIfNotNull("userId", String.valueOf(userId)); + } + sc.setParametersIfNotNull("id", id); + 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])); + + final 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..e00e4d55ff3b --- /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 org.apache.cloudstack.acl.ApiKeyPairPermissionVO; +import org.springframework.stereotype.Component; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +@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/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 8ab60a766246..0f75cea708f8 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 @@ -289,4 +289,6 @@ + + diff --git a/engine/schema/src/main/resources/META-INF/db/schema-41910to42000.sql b/engine/schema/src/main/resources/META-INF/db/schema-41910to42000.sql index f59eda5c06c9..c2e96443190e 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-41910to42000.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-41910to42000.sql @@ -154,3 +154,37 @@ WHERE -- Quota inject tariff result into subsequent ones CALL `cloud_usage`.`IDEMPOTENT_ADD_COLUMN`('cloud_usage.quota_tariff', 'position', 'bigint(20) NOT NULL DEFAULT 1 COMMENT "Position in the execution sequence for tariffs of the same type"'); + + +-- 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 7eedc03712b6..10b6531287a6 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/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 db40b6e68ddd..3af00bb3160c 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 @@ -17,15 +17,18 @@ package org.apache.cloudstack.acl; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; import javax.inject.Inject; import javax.naming.ConfigurationException; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPairPermission; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.acl.RolePermissionEntity.Permission; @@ -63,9 +66,12 @@ public List 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); } } @@ -80,8 +86,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; } @@ -96,11 +102,12 @@ public boolean checkApiPermissionByRole(Role role, String apiName, 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)); } + 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; + } + /** * Only one strategy should be used between StaticRoleBasedAPIAccessChecker and DynamicRoleBasedAPIAccessChecker * Default behavior is to use the Dynamic version. The StaticRoleBasedAPIAccessChecker is the legacy version. 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..53350fc1dac1 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,69 @@ 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..b17c1d31d5df 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 @@ -27,6 +27,7 @@ import com.cloud.exception.UnavailableCommandException; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPairPermission; import org.apache.cloudstack.api.APICommand; import com.cloud.exception.PermissionDeniedException; @@ -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/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..7f47f534a3f1 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/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 151344575dcc..9b22199895b8 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 @@ -40,6 +40,7 @@ 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.annotation.AnnotationService; @@ -1384,8 +1385,9 @@ private String[] getServiceUserKeys(KubernetesClusterVO kubernetesCluster) { KUBEADMIN_ACCOUNT_NAME, "kubeadmin", null, UUID.randomUUID().toString(), User.Source.UNKNOWN)); keys = accountService.createApiKeyAndSecretKey(kube.getId()); } else { - String apiKey = kubeadmin.getApiKey(); - String secretKey = kubeadmin.getSecretKey(); + ApiKeyPairVO latestKeypair = ApiDBUtils.searchForLatestUserKeyPair(kubeadmin.getId()); + String apiKey = latestKeypair.getApiKey(); + String secretKey = latestKeypair.getSecretKey(); if (StringUtils.isAnyEmpty(apiKey, secretKey)) { keys = accountService.createApiKeyAndSecretKey(kubeadmin.getId()); } else { 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 cb366959327a..c43f325ca556 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; @@ -113,6 +114,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,8 +1184,9 @@ 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(); + ApiKeyPairVO latestKeypair = ApiDBUtils.searchForLatestUserKeyPair(callerUser.getId()); + String apiKey = latestKeypair.getApiKey(); + String secretKey = latestKeypair.getSecretKey(); if (apiKey != null && secretKey != null) { String url; try { diff --git a/server/src/main/java/com/cloud/api/ApiDBUtils.java b/server/src/main/java/com/cloud/api/ApiDBUtils.java index 7d6a1276d9ae..27b93a63607c 100644 --- a/server/src/main/java/com/cloud/api/ApiDBUtils.java +++ b/server/src/main/java/com/cloud/api/ApiDBUtils.java @@ -32,6 +32,8 @@ import org.apache.cloudstack.acl.Role; import org.apache.cloudstack.acl.RoleService; +import org.apache.cloudstack.acl.ApiKeyPairVO; +import org.apache.cloudstack.acl.dao.ApiKeyPairDao; import org.apache.cloudstack.affinity.AffinityGroup; import org.apache.cloudstack.affinity.AffinityGroupResponse; import org.apache.cloudstack.affinity.dao.AffinityGroupDao; @@ -493,6 +495,7 @@ public class ApiDBUtils { static BucketDao s_bucketDao; static VirtualMachineManager s_virtualMachineManager; + static ApiKeyPairDao s_apiKeyPairDao; @Inject private ManagementServer ms; @@ -758,6 +761,8 @@ public class ApiDBUtils { private BucketDao bucketDao; @Inject private VirtualMachineManager virtualMachineManager; + @Inject + private ApiKeyPairDao apiKeyPairDao; @PostConstruct void init() { @@ -893,6 +898,7 @@ void init() { s_objectStoreDao = objectStoreDao; s_bucketDao = bucketDao; s_virtualMachineManager = virtualMachineManager; + s_apiKeyPairDao = apiKeyPairDao; } // /////////////////////////////////////////////////////////// @@ -1959,6 +1965,10 @@ public static UserResponse newUserResponse(UserAccountJoinVO usr, Long domainId) 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 d247aaf53282..758097e40d8f 100644 --- a/server/src/main/java/com/cloud/api/ApiResponseHelper.java +++ b/server/src/main/java/com/cloud/api/ApiResponseHelper.java @@ -45,6 +45,8 @@ import com.cloud.storage.BucketVO; import org.apache.cloudstack.acl.ControlledEntity; import org.apache.cloudstack.acl.ControlledEntity.ACLType; +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.annotation.AnnotationService; @@ -58,6 +60,7 @@ import org.apache.cloudstack.api.ResponseObject.ResponseView; import org.apache.cloudstack.api.command.user.job.QueryAsyncJobResultCmd; import org.apache.cloudstack.api.response.AccountResponse; +import org.apache.cloudstack.api.response.ApiKeyPairResponse; import org.apache.cloudstack.api.response.ApplicationLoadBalancerInstanceResponse; import org.apache.cloudstack.api.response.ApplicationLoadBalancerResponse; import org.apache.cloudstack.api.response.ApplicationLoadBalancerRuleResponse; @@ -68,6 +71,7 @@ import org.apache.cloudstack.api.response.BackupOfferingResponse; 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.CapabilityResponse; import org.apache.cloudstack.api.response.CapacityResponse; @@ -5281,4 +5285,60 @@ public BucketResponse createBucketResponse(Bucket bucket) { populateAccount(bucketResponse, bucket.getAccountId()); return bucketResponse; } + @Override + public ApiKeyPairResponse createKeyPairResponse(ApiKeyPair keyPair) { + ApiKeyPairResponse apiKeyPairResponse = new ApiKeyPairResponse(); + + User user = ApiDBUtils.findUserById(keyPair.getUserId()); + apiKeyPairResponse.setName(keyPair.getName()); + apiKeyPairResponse.setApiKey(keyPair.getApiKey()); + apiKeyPairResponse.setSecretKey(keyPair.getSecretKey()); + apiKeyPairResponse.setDescription(keyPair.getDescription()); + apiKeyPairResponse.setUuid(keyPair.getUuid()); + apiKeyPairResponse.setCreated(keyPair.getCreated()); + apiKeyPairResponse.setStartDate(keyPair.getStartDate()); + apiKeyPairResponse.setEndDate(keyPair.getEndDate()); + apiKeyPairResponse.setUserId(keyPair.getUserId().toString()); + AccountJoinVO account = ApiDBUtils.findAccountViewById(user.getAccountId()); + apiKeyPairResponse.setUserId(String.valueOf(user.getId())); + apiKeyPairResponse.setUserName(user.getFirstname()); + apiKeyPairResponse.setAccountType(account.getType().ordinal()); + apiKeyPairResponse.setDomainId(account.getDomainUuid()); + apiKeyPairResponse.setDomainName(account.getDomainName()); + StringBuilder domainPath = new StringBuilder("ROOT"); + (domainPath.append(account.getDomainPath())).deleteCharAt(domainPath.length() - 1); + apiKeyPairResponse.setDomainPath(domainPath.toString()); + + Account.State state = Account.State.ENABLED; + if (keyPair.getRemoved() != null) { + state = Account.State.REMOVED; + } else if (!keyPair.validateDate(false)) { + state = Account.State.DISABLED; + } + apiKeyPairResponse.setState(state.toString()); + + // set async job + if (account.getJobId() != null) { + apiKeyPairResponse.setJobId(account.getJobUuid()); + apiKeyPairResponse.setJobStatus(account.getJobStatus()); + } + + return apiKeyPairResponse; + } + + @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 0d4382097c24..f2f066350868 100644 --- a/server/src/main/java/com/cloud/api/ApiServer.java +++ b/server/src/main/java/com/cloud/api/ApiServer.java @@ -55,7 +55,11 @@ import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; +import com.cloud.utils.Ternary; 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; @@ -221,6 +225,9 @@ public class ApiServer extends ManagerBase implements HttpRequestHandler, ApiSer @Inject private ApiAsyncJobDispatcher asyncDispatcher; + @Inject + private ApiKeyPairManagerImpl keyPairManager; + private EventDistributor eventDistributor = null; private static int s_workerCount = 0; @@ -969,18 +976,19 @@ public boolean verifyRequest(final Map requestParameters, fina txn.close(); User user = null; // 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, userid = " + user.getId() + "; name = " + user.getUsername() + "; state: " + user.getState() + - "; accountState: " + account.getState()); + logger.info(String.format("Disabled or locked user accessing the api, userid = %d; name = %s; state: %s; accountState: %s", + user.getId(), user.getUsername(), user.getState(), account.getState())); return false; } @@ -988,10 +996,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(true); + + secretKey = keyPair.getSecretKey(); if (secretKey == null) { - logger.info("User does not have a secret key associated with the account -- ignoring request, username: " + user.getUsername()); + logger.info(String.format("User does not have a secret key associated with the API key -- ignoring request, username: [%s].", user.getUsername())); return false; } @@ -1009,10 +1023,15 @@ public boolean verifyRequest(final Map requestParameters, fina if (!equalSig) { signature = signature.replaceAll(SANITIZATION_REGEX, "_"); logger.info(String.format("User signature [%s] is not equaled to computed signature [%s].", signature, computedSignature)); - } else { - CallContext.register(user, account); + return false; + } + CallContext.register(user, account); + + List keyPairPermissions = keyPairManager.findAllPermissionsByKeyPairId(keyPair.getId()); + + if (commandAvailable(remoteAddress, commandName, user, keyPairPermissions.toArray(new ApiKeyPairPermission[0]))) { + return true; } - return equalSig; } catch (final ServerApiException ex) { throw ex; } catch (final Exception ex) { @@ -1021,9 +1040,9 @@ public boolean verifyRequest(final Map requestParameters, fina 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()); @@ -1223,7 +1242,8 @@ public boolean verifyUser(final Long userId) { return true; } - 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); } @@ -1237,12 +1257,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/query/QueryManagerImpl.java b/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java index a39299d5091e..baca3191215e 100644 --- a/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java +++ b/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java @@ -38,7 +38,10 @@ import org.apache.cloudstack.acl.ControlledEntity; import org.apache.cloudstack.acl.ControlledEntity.ACLType; +import org.apache.cloudstack.acl.RoleService; +import org.apache.cloudstack.acl.RoleVO; import org.apache.cloudstack.acl.SecurityChecker; +import org.apache.cloudstack.acl.dao.RoleDao; import org.apache.cloudstack.affinity.AffinityGroupDomainMapVO; import org.apache.cloudstack.affinity.AffinityGroupResponse; import org.apache.cloudstack.affinity.AffinityGroupVMMapVO; @@ -351,6 +354,12 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q @Inject private AccountManager accountMgr; + @Inject + private RoleService roleService; + + @Inject + private RoleDao roleDao; + @Inject private ProjectManager _projectMgr; @@ -802,6 +811,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, callingAccount.getDomainId(), true, searchFilter); + 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/UserAccountJoinDaoImpl.java b/server/src/main/java/com/cloud/api/query/dao/UserAccountJoinDaoImpl.java index c5b21f50d2d9..618be46244cc 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 @@ -19,7 +19,9 @@ import java.util.List; +import com.cloud.api.ApiDBUtils; import com.cloud.user.AccountManagerImpl; +import org.apache.cloudstack.acl.ApiKeyPairVO; import org.springframework.stereotype.Component; import org.apache.cloudstack.api.response.UserResponse; @@ -68,14 +70,16 @@ public UserResponse newUserResponse(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()); long domainId = usr.getDomainId(); boolean is2FAmandated = Boolean.TRUE.equals(AccountManagerImpl.enableUserTwoFactorAuthentication.valueIn(domainId)) && Boolean.TRUE.equals(AccountManagerImpl.mandateUserTwoFactorAuthentication.valueIn(domainId)); userResponse.set2FAmandated(is2FAmandated); + ApiKeyPairVO lastKeyPair = ApiDBUtils.searchForLatestUserKeyPair(usr.getId()); + userResponse.setApiKey(lastKeyPair.getApiKey()); + userResponse.setSecretKey(lastKeyPair.getSecretKey()); + // set async job if (usr.getJobId() != null) { userResponse.setJobId(usr.getJobUuid()); 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 3a82980725b3..e5b73c5bb3a6 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 @@ -61,13 +61,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; @@ -206,14 +199,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 4776cfc62468..5c8e98790a7c 100644 --- a/server/src/main/java/com/cloud/network/as/AutoScaleManagerImpl.java +++ b/server/src/main/java/com/cloud/network/as/AutoScaleManagerImpl.java @@ -37,6 +37,7 @@ import javax.inject.Inject; +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; @@ -506,8 +507,9 @@ 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 apiKey = latestKeypair.getApiKey(); + String secretKey = latestKeypair.getSecretKey(); String csUrl = ApiServiceConfiguration.ApiServletPath.value(); if (apiKey == null) { 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 6edb9c4f0afa..ba19fcb87ea1 100644 --- a/server/src/main/java/com/cloud/network/lb/LoadBalancingRulesManagerImpl.java +++ b/server/src/main/java/com/cloud/network/lb/LoadBalancingRulesManagerImpl.java @@ -29,8 +29,10 @@ import javax.inject.Inject; +import com.cloud.api.ApiDBUtils; 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; @@ -348,8 +350,9 @@ 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(); + ApiKeyPairVO latestKeypair = ApiDBUtils.searchForLatestUserKeyPair(user.getId()); + apiKey = latestKeypair.getApiKey(); + secretKey = latestKeypair.getSecretKey(); if (apiKey == null) { throw new InvalidParameterValueException("apiKey for user: " + user.getUsername() + " is empty. Please generate it"); } 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 52a488fad822..98deb67ef72d 100644 --- a/server/src/main/java/com/cloud/network/router/VirtualNetworkApplianceManagerImpl.java +++ b/server/src/main/java/com/cloud/network/router/VirtualNetworkApplianceManagerImpl.java @@ -48,6 +48,8 @@ import javax.inject.Inject; import javax.naming.ConfigurationException; +import com.cloud.api.ApiDBUtils; +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; @@ -2093,8 +2095,9 @@ 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()); + 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 a485cfe0c66c..74c66d1ba5a4 100644 --- a/server/src/main/java/com/cloud/server/ManagementServerImpl.java +++ b/server/src/main/java/com/cloud/server/ManagementServerImpl.java @@ -43,6 +43,7 @@ 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.affinity.AffinityGroupProcessor; @@ -269,10 +270,13 @@ 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.DeleteUserCmd; +import org.apache.cloudstack.api.command.admin.user.DeleteUserKeysCmd; 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; @@ -4020,6 +4024,9 @@ public List> getCommands() { cmdList.add(IssueOutOfBandManagementPowerActionCmd.class); cmdList.add(ChangeOutOfBandManagementPasswordCmd.class); cmdList.add(GetUserKeysCmd.class); + cmdList.add(ListUserKeysCmd.class); + cmdList.add(ListUserKeyRulesCmd.class); + cmdList.add(DeleteUserKeysCmd.class); cmdList.add(CreateConsoleEndpointCmd.class); //user data APIs @@ -4407,7 +4414,8 @@ 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()); + 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 ad884a33406e..330b10fe352c 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; @@ -535,14 +536,15 @@ 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, userid = " + user.getId() + "; name = " + user.getUsername() + "; state: " + user.getState() + @@ -550,10 +552,8 @@ private boolean verifyRequest(Map requestParameters) { 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, username: " + user.getUsername()); + 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 6d2d1db5668d..b94a1228e690 100644 --- a/server/src/main/java/com/cloud/user/AccountManager.java +++ b/server/src/main/java/com/cloud/user/AccountManager.java @@ -22,6 +22,7 @@ import com.cloud.api.auth.SetupUserTwoFactorAuthenticationCmd; 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; @@ -85,7 +86,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); + public 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 15121aa0a143..d5a1ecc54df2 100644 --- a/server/src/main/java/com/cloud/user/AccountManagerImpl.java +++ b/server/src/main/java/com/cloud/user/AccountManagerImpl.java @@ -22,6 +22,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; +import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashSet; @@ -42,13 +43,24 @@ import javax.naming.ConfigurationException; 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; @@ -58,9 +70,12 @@ 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.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.UpdateUserCmd; +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.auth.UserAuthenticator; import org.apache.cloudstack.auth.UserAuthenticator.ActionOnFailedAuthentication; @@ -73,6 +88,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.region.gslb.GlobalLoadBalancerRuleDao; import org.apache.cloudstack.resourcedetail.UserDetailVO; import org.apache.cloudstack.resourcedetail.dao.UserDetailsDao; @@ -81,8 +97,10 @@ 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.joda.time.DateTime; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import com.cloud.api.ApiDBUtils; @@ -219,6 +237,16 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M @Inject private UserAccountDao _userAccountDao; @Inject + private ApiKeyPairDao apiKeyPairDao; + @Inject + private ApiKeyPairService apiKeyPairService; + @Inject + private ApiKeyPairManagerImpl keyPairManager; + @Inject + private QueryService queryService; + @Inject + private ApiKeyPairPermissionsDao apiKeyPairPermissionsDao; + @Inject private VolumeDao _volumeDao; @Inject private UserVmDao _userVmDao; @@ -369,8 +397,13 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M "totp", "The default user two factor authentication provider. Eg. totp, staticpin", true, ConfigKey.Scope.Domain); + private Map> annotationRoleBasedApisMap = new HashMap<>(); + protected AccountManagerImpl() { super(); + for (RoleType roleType : RoleType.values()) { + annotationRoleBasedApisMap.put(roleType, new HashSet<>()); + } } public List getUserAuthenticators() { @@ -1433,7 +1466,7 @@ public UserAccount updateUser(UpdateUserCmd updateUserCmd) { UserVO user = retrieveAndValidateUser(updateUserCmd); logger.debug("Updating user with Id: " + user.getUuid()); - validateAndUpdateApiAndSecretKeyIfNeeded(updateUserCmd, user); + ApiKeyPairVO keyPair = validateAndUpdateApiAndSecretKeyIfNeeded(updateUserCmd, user); Account account = retrieveAndValidateAccount(user); validateAndUpdateFirstNameIfNeeded(updateUserCmd, user); @@ -1453,6 +1486,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()); } @@ -1629,7 +1665,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(); @@ -1639,17 +1675,19 @@ 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(); + Ternary keyPairTernary = _accountDao.findUserAccountByApiKey(apiKey); + if (keyPairTernary != null) { + 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)); } } - user.setApiKey(apiKey); - user.setSecretKey(secretKey); + ApiKeyPairVO keyPair = (ApiKeyPairVO) keyPairTernary.third(); + keyPair.setApiKey(apiKey); + keyPair.setSecretKey(secretKey); + return keyPair; } /** @@ -2136,8 +2174,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()); @@ -2752,18 +2788,13 @@ protected void updateLoginAttemptsWhenIncorrectLoginAttemptsEnabled(UserAccount } @Override - public Pair findUserByApiKey(String apiKey) { + public Ternary findUserByApiKey(String apiKey) { return _accountDao.findUserAccountByApiKey(apiKey); } @Override public Map getKeys(GetUserKeysCmd cmd) { final long userId = cmd.getID(); - return getKeys(userId); - } - - @Override - public Map getKeys(Long userId) { User user = getActiveUser(userId); if (user == null) { throw new InvalidParameterValueException("Unable to find user by id"); @@ -2772,12 +2803,76 @@ public Map getKeys(Long userId) { checkAccess(CallContext.current().getCallingUser(), account); Map keys = new HashMap(); - keys.put("apikey", user.getApiKey()); - keys.put("secretkey", user.getSecretKey()); + + ApiKeyPair keyPair = getLatestUserKeyPair(cmd.getID()); + keys.put("apikey", keyPair.getApiKey()); + keys.put("secretkey", keyPair.getSecretKey()); return keys; } + public ListResponse getKeys(ListUserKeysCmd cmd) { + ListResponse finalResponse = new ListResponse<>(); + List responses = new ArrayList<>(); + + if (!cmd.listAll() || cmd.getKeyId() != null) { + fetchOnlyOneKeypair(responses, cmd); + finalResponse.setResponses(responses); + return finalResponse; + } + Integer total = fetchMultipleKeypairs(responses, cmd); + finalResponse.setResponses(responses, total); + return finalResponse; + } + + public ApiKeyPair getLatestUserKeyPair(Long userId) { + return ApiDBUtils.searchForLatestUserKeyPair(userId); + } + + private void fetchOnlyOneKeypair(List responses, ListUserKeysCmd cmd) { + ApiKeyPair keyPair; + if (cmd.getKeyId() != null) { + keyPair = _accountService.getKeyPairById(cmd.getKeyId()); + } else if (cmd.getUserId() != null) { + keyPair = _accountService.getLatestUserKeyPair(cmd.getUserId()); + } else { + keyPair = _accountService.getLatestUserKeyPair(CallContext.current().getCallingUser().getId()); + } + apiKeyPairService.validateCallingUserHasAccessToDesiredUser(keyPair.getUserId()); + addKeypairResponse(keyPair, responses, cmd); + } + + private Integer fetchMultipleKeypairs(List responses, ListUserKeysCmd cmd) { + List users; + if (cmd.getUserId() != null) { + apiKeyPairService.validateCallingUserHasAccessToDesiredUser(cmd.getUserId()); + users = List.of(cmd.getUserId()); + } else { + User callerUser = CallContext.current().getCallingUser(); + users = isRootAdmin(callerUser.getAccountId()) ? queryService.searchForAccessableUsers() : List.of(callerUser.getId()); + } + + Pair, Integer> keyPairs = apiKeyPairDao.listByUserIdsPaginated(users, cmd); + for (ApiKeyPairVO keyPair : keyPairs.first()) { + addKeypairResponse(keyPair, responses, cmd); + } + return keyPairs.second(); + } + + private void addKeypairResponse(ApiKeyPair keyPair, List responses, ListUserKeysCmd cmd) { + if (keyPair == null) { + return; + } + ApiKeyPairResponse response = cmd._responseGenerator.createKeyPairResponse(keyPair); + response.setObjectName(ApiConstants.USER_API_KEY); + responses.add(response); + } + + @Override + public ApiKeyPair getKeyPairById(Long id) { + return apiKeyPairDao.findById(id); + } + @Override public List listUserTwoFactorAuthenticationProviders() { return userTwoFactorAuthenticationProviders; @@ -2802,40 +2897,55 @@ 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(RegisterCmd 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(); + Account account = _accountDao.findById(user.getAccountId()); checkAccess(caller, null, true, account); - // don't allow updating system user - if (user.getId() == User.UID_SYSTEM) { - throw new PermissionDeniedException("user id : " + user.getId() + " is system account, update is not allowed"); + // don't allow baremetal or system user + if (BaremetalUtils.BAREMETAL_SYSTEM_ACCOUNT_NAME.equals(user.getUsername()) || user.getId() == User.UID_SYSTEM) { + throw new PermissionDeniedException("User id : " + user.getId() + " is system account, update is not allowed."); + } + + 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."); } - // don't allow baremetal system user - if (BaremetalUtils.BAREMETAL_SYSTEM_ACCOUNT_NAME.equals(user.getUsername())) { - throw new PermissionDeniedException("user id : " + user.getId() + " is system account, update is not allowed"); + 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]; + // this is only used to determine if we should use api key permissions or account permissions to base our new key on + Boolean accessedByApiKey = cmd.getFullUrlParams().containsKey(ApiConstants.SIGNATURE); + String apiKey = cmd.getFullUrlParams().get("apiKey"); + + // 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)}; Transaction.execute(new TransactionCallbackNoReturn() { @Override public void doInTransactionWithoutResult(TransactionStatus status) { - keys[0] = createUserApiKey(userId); - keys[1] = createUserSecretKey(userId); + createUserApiKey(userId, newApiKeyPair[0]); + createUserSecretKey(userId, newApiKeyPair[0]); + newApiKeyPair[0] = validateAndPersistKeyPairAndPermissions(account, newApiKeyPair[0], rules, accessedByApiKey, apiKey); } }); - - return keys; + return newApiKeyPair[0]; } + @Override @DB @ActionEvent(eventType = EventTypes.EVENT_REGISTER_FOR_SECRET_API_KEY, eventDescription = "register for the developer API keys") @@ -2848,37 +2958,41 @@ 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) { + private String createUserApiKey(long userId, ApiKeyPairVO newApiKeyPair) { try { - UserVO updatedUser = _userDao.createForUpdate(); - String encodedKey = null; - Pair userAcct = null; + ApiKeyPair keyPair = null; 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 id=" + userId, ex); @@ -2886,26 +3000,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 = null; int retryLimit = 10; - UserVO userBySecretKey = null; + ApiKeyPairVO keyPairVO = null; 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 id=" + userId, ex); @@ -2913,6 +3025,105 @@ private String createUserSecretKey(long userId) { return null; } + /*** + * 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, Boolean accessedByApiKey, String apiKey) { + if (newApiKeyPair.getName() == null) { + User user = _userDao.findById(newApiKeyPair.getUserId()); + newApiKeyPair.setName(user.getUsername() + " - API Keypair"); + } + final ApiKeyPairVO savedApiKeyPair = apiKeyPairDao.persist(newApiKeyPair); + + final Role accountRole = roleService.findRole(account.getRoleId()); + + List allPermissions = (accessedByApiKey == null || BooleanUtils.isFalse(accessedByApiKey)) ? getAllAccountRolePermissions(accountRole) : getAllKeypairPermissions(apiKey); + + List validatedPermissions; + + if (CollectionUtils.isEmpty(rules)) { + validatedPermissions = allPermissions.stream().map(permission -> new ApiKeyPairPermissionVO(savedApiKeyPair.getId(), + permission.getRule().toString(), permission.getPermission(), permission.getDescription())).collect(Collectors.toList()); + } else { + validatedPermissions = validatePermissions(rules, savedApiKeyPair, allPermissions); + } + + validatedPermissions.forEach(permission -> apiKeyPairPermissionsDao.persist(permission)); + 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 + * @return + */ + 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); + List allApiKeyRolePermissions = keyPairManager.findAllPermissionsByKeyPairId(apiKeyPair.getId()); + return allApiKeyRolePermissions.stream().map(permission -> (RolePermissionEntity) permission) + .collect(Collectors.toList()); + } + + /*** + * Validates if the user permissions are a superset of permissions of the Keypair that is being created + * @param rules are the rules passed to the API which are being validated + * @param savedApiKeyPair is the new keypair being created + * @throws InvalidParameterValueException if the user's permissions are not a superset of the Keypair. + */ + private List validatePermissions(List> rules, ApiKeyPairVO savedApiKeyPair, + List allAccountPermissions) { + + List validatedPermissions = new ArrayList<>(); + for (Map ruleDetail : rules) { + String apiName = ruleDetail.get(ApiConstants.RULE).toString(); + RolePermission.Permission rulePermission = (RolePermission.Permission) ruleDetail.get(ApiConstants.PERMISSION); + String ruleDescription = (String) ruleDetail.get(ApiConstants.DESCRIPTION); + + matchARuleWithAccountRules(allAccountPermissions, apiName, rulePermission, savedApiKeyPair, ruleDescription, validatedPermissions); + } + return validatedPermissions; + } + + private void matchARuleWithAccountRules(List allAccountPermissions, String apiName, RolePermission.Permission rulePermission, + ApiKeyPairVO savedApiKeyPair, String ruleDescription, List validatedPermissions) { + for (RolePermissionEntity accountPermission : allAccountPermissions) { + if (!accountPermission.getRule().matches(apiName)) { + continue; + } + + if (!accountPermission.getPermission().equals(RolePermissionEntity.Permission.ALLOW) && rulePermission.equals(RolePermissionEntity.Permission.ALLOW)) { + apiKeyPairDao.remove(savedApiKeyPair.getId()); + throw new InvalidParameterValueException(String.format("API %s is not allowed based on the user's account, a valid " + + "keypair rule must be also allowed at the account level", apiName)); + } + validatedPermissions.add(new ApiKeyPairPermissionVO(savedApiKeyPair.getId(), apiName, rulePermission, ruleDescription)); + return; + } + apiKeyPairDao.remove(savedApiKeyPair.getId()); + throw new InvalidParameterValueException(String.format("API %s was not found on the user's account, a valid " + + "keypair rule must be also present at the account level", apiName)); + } + @Override public void buildACLSearchBuilder(SearchBuilder sb, Long domainId, boolean isRecursive, List permittedAccounts, ListProjectResourcesCriteria listProjectResourcesCriteria) { @@ -3119,7 +3330,8 @@ public void buildACLViewSearchCriteria(SearchCriteria findAllPermissionsByKeyPairId(Long apiKeyPairId) { + List allPermissions = apiKeyPairPermissionsDao.findAllByKeyPairIdSorted(apiKeyPairId); + if (allPermissions != null) { + return allPermissions.stream().map(p -> (ApiKeyPairPermission) p).collect(Collectors.toList()); + } + return Collections.emptyList(); + } + + @Override + public ApiKeyPair findByApiKey(String apiKey) { + return apiKeyPairDao.findByApiKey(apiKey); + } + + @Override + public ApiKeyPair findById(Long id) { + return apiKeyPairDao.findById(id); + } + + @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) { + throw new PermissionDeniedException("User id : " + user.getUuid() + " is system account, deletion of API Keys is not allowed."); + } + + List permissions = apiKeyPairPermissionsDao.findAllByApiKeyPairId(keyPair.getId()); + for (ApiKeyPairPermission permission : permissions) { + apiKeyPairPermissionsDao.remove(permission.getId()); + } + apiKeyPairDao.remove(keyPair.getId()); + } + + @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())); + } + User callerUser = CallContext.current().getCallingUser(); + if (!accountManager.isAdmin(callerUser.getAccountId()) && callerUser.getId() != userId) { + throw new PermissionDeniedException("Only root admin can operate on API keys owned by other users"); + } + } +} 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 bab286bb8f99..03536bd9e9fe 100644 --- a/server/src/main/java/org/apache/cloudstack/acl/RoleManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/acl/RoleManagerImpl.java @@ -417,7 +417,8 @@ public Pair, Integer> findRolesByName(String name, String keyword, Lo /** * 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; } diff --git a/server/src/test/java/com/cloud/user/AccountManagerImplTest.java b/server/src/test/java/com/cloud/user/AccountManagerImplTest.java index e5c623ca6df7..b24ff6f820ac 100644 --- a/server/src/test/java/com/cloud/user/AccountManagerImplTest.java +++ b/server/src/test/java/com/cloud/user/AccountManagerImplTest.java @@ -16,19 +16,38 @@ // under the License. package com.cloud.user; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.nullable; import java.net.InetAddress; import java.net.UnknownHostException; +import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; +import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; +import com.cloud.utils.Ternary; +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.RoleService; +import org.apache.cloudstack.acl.RoleVO; +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.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.RegisterCmd; import org.apache.cloudstack.api.command.admin.user.UpdateUserCmd; import org.apache.cloudstack.api.response.UserTwoFactorAuthenticationSetupResponse; import org.apache.cloudstack.auth.UserAuthenticator; @@ -79,7 +98,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 @@ -92,6 +113,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 @@ -114,6 +144,19 @@ public class AccountManagerImplTest extends AccountManagetImplTestBase { @Mock ConfigKey enableUserTwoFactorAuthenticationMock; + @Mock + ApiKeyPairPermissionsDao keyPairPermissionsDaoMock; + + @Mock + ApiKeyPairDao keyPairDaoMock; + + @Mock + RoleService roleServiceMock; + + @Mock + RegisterCmd registerCmdMock; + + @Before public void setUp() throws Exception { enableUserTwoFactorAuthenticationMock = Mockito.mock(ConfigKey.class); @@ -128,6 +171,13 @@ public void beforeTest() { Mockito.doReturn(accountMockId).when(userVoMock).getAccountId(); Mockito.doReturn(userVoIdMock).when(userVoMock).getId(); + + Mockito.when(keyPairDaoMock.persist(Mockito.any())).thenAnswer(i -> { + ApiKeyPairVO keyPair = (ApiKeyPairVO) i.getArguments()[0]; + keyPair.setId(1L); + return keyPair; + }); + } @Test @@ -276,10 +326,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(accountManagerImpl.getActiveUser(1L)).thenReturn(userVoMock); - Mockito.when(userAccountDaoMock.findById(1L)).thenReturn(userAccountVO); - Mockito.when(userAccountVO.getAccountId()).thenReturn(1L); + Mockito.when(listUserKeysCmd.getKeyId()).thenReturn(null); + Mockito.when(listUserKeysCmd.getUserId()).thenReturn(1L); + Mockito.doThrow(new PermissionDeniedException("PDE")).when(apiKeyPairService).validateCallingUserHasAccessToDesiredUser(Mockito.anyLong()); Mockito.lenient().when(accountManagerImpl.getAccount(Mockito.anyLong())).thenReturn(accountMock); // Queried account - admin account Mockito.lenient().when(callingUser.getAccountId()).thenReturn(1L); @@ -288,7 +337,7 @@ 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(listUserKeysCmd); } @Test @@ -307,7 +356,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); @@ -386,7 +435,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); @@ -405,14 +454,14 @@ 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); 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(expected = CloudRuntimeException.class) @@ -1058,4 +1107,416 @@ public void testDeleteWebhooksForAccountNoBean() { accountManagerImpl.deleteWebhooksForAccount(1L); } } + + @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(keyPairDaoMock.findBySecretKey(Mockito.anyString())).thenReturn(null); + Mockito.when(roleServiceMock.findAllPermissionsBy(Mockito.anyLong())).thenReturn(List.of( + new RolePermissionVO(1L, "api2", RolePermissionEntity.Permission.ALLOW, "description") + )); + Mockito.when(roleServiceMock.findRole(Mockito.anyLong())).thenReturn(new RoleVO()); + + accountManagerImpl.createApiKeyAndSecretKey(registerCmdMock); + Mockito.verify(keyPairDaoMock, Mockito.times(0)).remove(Mockito.anyLong()); + Mockito.verify(keyPairPermissionsDaoMock, 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(roleServiceMock.findAllPermissionsBy(Mockito.anyLong())).thenReturn(List.of( + new RolePermissionVO(1L, "api2", RolePermissionEntity.Permission.ALLOW, "description") + )); + Mockito.when(roleServiceMock.findRole(Mockito.anyLong())).thenReturn(new RoleVO()); + + accountManagerImpl.createApiKeyAndSecretKey(registerCmdMock); + Mockito.verify(keyPairDaoMock, Mockito.times(1)).remove(Mockito.anyLong()); + Mockito.verify(keyPairPermissionsDaoMock, 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(keyPairDaoMock.findBySecretKey(Mockito.anyString())).thenReturn(null); + Mockito.when(roleServiceMock.findAllPermissionsBy(Mockito.anyLong())).thenReturn(List.of( + new RolePermissionVO(1L, "api", RolePermissionEntity.Permission.DENY, "description") + )); + Mockito.when(roleServiceMock.findRole(Mockito.anyLong())).thenReturn(new RoleVO()); + + accountManagerImpl.createApiKeyAndSecretKey(registerCmdMock); + Mockito.verify(keyPairDaoMock, Mockito.times(1)).remove(Mockito.anyLong()); + Mockito.verify(keyPairPermissionsDaoMock, 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); + + + 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(keyPairPermissionsDaoMock.persist(Mockito.any(ApiKeyPairPermissionVO.class))).thenReturn(permissionVO); + Mockito.when(keyPairDaoMock.findBySecretKey(Mockito.anyString())).thenReturn(null); + Mockito.when(roleServiceMock.findAllPermissionsBy(Mockito.anyLong())).thenReturn(List.of( + new RolePermissionVO(1L, "api", RolePermissionEntity.Permission.ALLOW, "description") + )); + Mockito.when(roleServiceMock.findRole(Mockito.anyLong())).thenReturn(new RoleVO()); + + Assert.assertEquals((long) accountManagerImpl.createApiKeyAndSecretKey(registerCmdMock).getUserId(), userId); + Mockito.verify(keyPairPermissionsDaoMock, Mockito.times(1)).persist(Mockito.any(ApiKeyPairPermissionVO.class)); + } + + @Test + public void createApiKeyAndSecretKeyTestWithDeniedThatIsAllowedOnAccount() { + 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); + + 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(keyPairPermissionsDaoMock.persist(Mockito.any(ApiKeyPairPermissionVO.class))).thenReturn(permissionVO); + Mockito.when(keyPairDaoMock.findBySecretKey(Mockito.anyString())).thenReturn(null); + Mockito.when(roleServiceMock.findAllPermissionsBy(Mockito.anyLong())).thenReturn(List.of( + new RolePermissionVO(1L, "api", RolePermissionEntity.Permission.ALLOW, "description") + )); + Mockito.when(roleServiceMock.findRole(Mockito.anyLong())).thenReturn(new RoleVO()); + + ApiKeyPair keyPairVO = accountManagerImpl.createApiKeyAndSecretKey(registerCmdMock); + + Assert.assertEquals((long) keyPairVO.getUserId(), userId); + } + + @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(keyPairPermissionsDaoMock.persist(Mockito.any(ApiKeyPairPermissionVO.class))).thenReturn(permissionVO); + Mockito.when(keyPairDaoMock.findBySecretKey(Mockito.anyString())).thenReturn(null); + Mockito.when(roleServiceMock.findAllPermissionsBy(Mockito.anyLong())).thenReturn(List.of( + new RolePermissionVO(1L, "api", RolePermissionEntity.Permission.ALLOW, "description") + )); + Mockito.when(roleServiceMock.findRole(Mockito.anyLong())).thenReturn(new RoleVO()); + + ApiKeyPair response = accountManagerImpl.createApiKeyAndSecretKey(registerCmdMock); + Mockito.verify(keyPairDaoMock, Mockito.times(1)).persist(Mockito.any(ApiKeyPairVO.class)); + 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 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((long) response.getUserId(), userId); + Assert.assertEquals(response.getStartDate(), startDate); + Assert.assertEquals(response.getEndDate(), endDate); + Assert.assertEquals(response.getDescription(), "key description"); + } + + @Test + public void createApiAndSecretKeyTestWithMultipleAllowedPermissions() { + 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" + )); + Mockito.when(registerCmdMock.getRules()).thenReturn(rules); + Mockito.when(registerCmdMock.getUserId()).thenReturn(userId); + + 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(keyPairPermissionsDaoMock.persist(Mockito.any(ApiKeyPairPermissionVO.class))).thenReturn(permissionVO); + Mockito.when(keyPairDaoMock.findBySecretKey(Mockito.anyString())).thenReturn(null); + Mockito.when(roleServiceMock.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(roleServiceMock.findRole(Mockito.anyLong())).thenReturn(new RoleVO()); + + accountManagerImpl.createApiKeyAndSecretKey(registerCmdMock); + Mockito.verify(keyPairDaoMock, Mockito.times(0)).remove(Mockito.anyLong()); + Mockito.verify(keyPairPermissionsDaoMock, Mockito.times(2)).persist(Mockito.any(ApiKeyPairPermissionVO.class)); + } + + @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(keyPairDaoMock.findBySecretKey(Mockito.anyString())).thenReturn(null); + Mockito.when(roleServiceMock.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(roleServiceMock.findRole(Mockito.anyLong())).thenReturn(new RoleVO()); + + accountManagerImpl.createApiKeyAndSecretKey(registerCmdMock); + Mockito.verify(keyPairDaoMock, Mockito.times(1)).remove(Mockito.anyLong()); + Mockito.verify(keyPairPermissionsDaoMock, 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(keyPairDaoMock.findBySecretKey(Mockito.anyString())).thenReturn(null); + Mockito.when(roleServiceMock.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(roleServiceMock.findRole(Mockito.anyLong())).thenReturn(new RoleVO()); + + accountManagerImpl.createApiKeyAndSecretKey(registerCmdMock); + Mockito.verify(keyPairDaoMock, Mockito.times(0)).remove(Mockito.anyLong()); + Mockito.verify(keyPairPermissionsDaoMock, Mockito.times(0)).persist(Mockito.any(ApiKeyPairPermissionVO.class)); + } } diff --git a/server/src/test/java/com/cloud/user/MockAccountManagerImpl.java b/server/src/test/java/com/cloud/user/MockAccountManagerImpl.java index 334e1f334818..3ef91f7c05de 100644 --- a/server/src/test/java/com/cloud/user/MockAccountManagerImpl.java +++ b/server/src/test/java/com/cloud/user/MockAccountManagerImpl.java @@ -37,13 +37,17 @@ 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.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.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.UpdateUserCmd; +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.auth.UserTwoFactorAuthenticator; import org.apache.cloudstack.framework.config.ConfigKey; @@ -306,12 +310,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(RegisterCmd cmd) { return null; } @@ -450,7 +454,7 @@ public Map getKeys(GetUserKeysCmd cmd) { } @Override - public Map getKeys(Long userId) { + public ListResponse getKeys(ListUserKeysCmd cmd) { return null; } @@ -464,6 +468,16 @@ 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 void checkAccess(User user, ControlledEntity entity) throws PermissionDeniedException { diff --git a/server/src/test/java/org/apache/cloudstack/acl/ApiKeyPairManagerImplTest.java b/server/src/test/java/org/apache/cloudstack/acl/ApiKeyPairManagerImplTest.java new file mode 100644 index 000000000000..6d27d1f73d34 --- /dev/null +++ b/server/src/test/java/org/apache/cloudstack/acl/ApiKeyPairManagerImplTest.java @@ -0,0 +1,56 @@ +// 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.user.UserVO; +import com.cloud.user.dao.UserDao; +import org.apache.cloudstack.acl.dao.ApiKeyPairDao; +import org.apache.cloudstack.acl.dao.ApiKeyPairPermissionsDao; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.List; + +@RunWith(MockitoJUnitRunner.class) +public class ApiKeyPairManagerImplTest { + @Mock + ApiKeyPairPermissionsDao apiKeyPairPermissionsDao; + @Mock + ApiKeyPairDao apiKeyPairDao; + @Mock + UserDao userDao; + @InjectMocks + ApiKeyPairManagerImpl apiKeyPairManager; + + @Before + public void setup() { + Mockito.when(apiKeyPairPermissionsDao.findAllByApiKeyPairId(Mockito.any())).thenReturn(List.of(new ApiKeyPairPermissionVO())); + Mockito.when(userDao.findByIdIncludingRemoved(Mockito.any())).thenReturn(new UserVO()); + } + + @Test + public void deleteApiKeyTestOnePermission() { + apiKeyPairManager.deleteApiKey(new ApiKeyPairVO(1L, 1L)); + Mockito.verify(apiKeyPairPermissionsDao, Mockito.times(1)).remove(Mockito.anyLong()); + Mockito.verify(apiKeyPairDao, Mockito.times(1)).remove(Mockito.anyLong()); + } +} From 2e5de1e57d3ddc8ce8368890532a2646e1e7f481 Mon Sep 17 00:00:00 2001 From: "klaus.freitas.scclouds" Date: Tue, 30 Jul 2024 18:00:46 -0300 Subject: [PATCH 02/22] making some fixes --- .../main/java/com/cloud/user/UserAccount.java | 4 --- .../upgrade/dao/Upgrade41910to42000.java | 2 +- .../java/com/cloud/user/UserAccountVO.java | 1 - .../com/cloud/user/dao/AccountDaoImpl.java | 6 ++-- .../cloud/user/dao/UserAccountDaoImpl.java | 1 - .../acl/ApiKeyPairPermissionVO.java | 1 - .../main/java/com/cloud/api/ApiServer.java | 2 ++ .../api/query/dao/UserAccountJoinDaoImpl.java | 6 ++-- .../cloud/api/query/vo/UserAccountJoinVO.java | 1 - .../java/com/cloud/user/AccountManager.java | 1 - .../com/cloud/user/AccountManagerImpl.java | 1 - .../spring-server-core-managers-context.xml | 2 ++ .../network/as/AutoScaleManagerImplTest.java | 29 +++++++++++-------- .../cloud/user/AccountManagerImplTest.java | 11 +++---- .../cloud/user/MockAccountManagerImpl.java | 1 - 15 files changed, 33 insertions(+), 36 deletions(-) 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/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade41910to42000.java b/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade41910to42000.java index 175693e4b0d6..1c8a3a4fc859 100644 --- a/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade41910to42000.java +++ b/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade41910to42000.java @@ -90,7 +90,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) { 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 1673f714eeaf..cf3a01988488 100644 --- a/engine/schema/src/main/java/com/cloud/user/UserAccountVO.java +++ b/engine/schema/src/main/java/com/cloud/user/UserAccountVO.java @@ -33,7 +33,6 @@ import org.apache.cloudstack.api.InternalIdentity; -import com.cloud.utils.db.Encrypt; import com.cloud.utils.db.GenericDao; import org.apache.commons.lang3.StringUtils; 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 1c6927424026..e00089182ac6 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 @@ -161,10 +161,10 @@ public Ternary findUserAccountByApiKey(String apiKey) account.setState(State.getValueOf(rs.getString(10))); ApiKeyPairVO keyPair = new ApiKeyPairVO(rs.getLong(11)); - keyPair.setStartDate(rs.getDate(12)); - keyPair.setEndDate(rs.getDate(13)); + keyPair.setStartDate(rs.getTimestamp(12)); + keyPair.setEndDate(rs.getTimestamp(13)); keyPair.setSecretKey(DBEncryptionUtil.decrypt(rs.getString(14))); - keyPair.setRemoved(rs.getDate(15)); + keyPair.setRemoved(rs.getTimestamp(15)); userAcctTernary = new Ternary(user, account, keyPair); } 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 e0d3328e7377..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; 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 58121ed0847e..84fbb82c19ca 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 @@ -16,7 +16,6 @@ // under the License. package org.apache.cloudstack.acl; -import org.apache.cloudstack.acl.RolePermissionBaseVO; import org.apache.cloudstack.acl.apikeypair.ApiKeyPairPermission; import javax.persistence.Column; diff --git a/server/src/main/java/com/cloud/api/ApiServer.java b/server/src/main/java/com/cloud/api/ApiServer.java index f2f066350868..e0a9e5561c38 100644 --- a/server/src/main/java/com/cloud/api/ApiServer.java +++ b/server/src/main/java/com/cloud/api/ApiServer.java @@ -1034,6 +1034,8 @@ public boolean verifyRequest(final Map requestParameters, fina } } 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"); } 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 618be46244cc..7a0a27b55420 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 @@ -77,8 +77,10 @@ public UserResponse newUserResponse(UserAccountJoinVO usr) { userResponse.set2FAmandated(is2FAmandated); ApiKeyPairVO lastKeyPair = ApiDBUtils.searchForLatestUserKeyPair(usr.getId()); - userResponse.setApiKey(lastKeyPair.getApiKey()); - userResponse.setSecretKey(lastKeyPair.getSecretKey()); + if (lastKeyPair != null) { + userResponse.setApiKey(lastKeyPair.getApiKey()); + userResponse.setSecretKey(lastKeyPair.getSecretKey()); + } // set async job if (usr.getJobId() != null) { 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 e5b73c5bb3a6..77351cfd00fd 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; diff --git a/server/src/main/java/com/cloud/user/AccountManager.java b/server/src/main/java/com/cloud/user/AccountManager.java index b94a1228e690..f50b87c7ac74 100644 --- a/server/src/main/java/com/cloud/user/AccountManager.java +++ b/server/src/main/java/com/cloud/user/AccountManager.java @@ -36,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; diff --git a/server/src/main/java/com/cloud/user/AccountManagerImpl.java b/server/src/main/java/com/cloud/user/AccountManagerImpl.java index d5a1ecc54df2..96c6b70b8946 100644 --- a/server/src/main/java/com/cloud/user/AccountManagerImpl.java +++ b/server/src/main/java/com/cloud/user/AccountManagerImpl.java @@ -47,7 +47,6 @@ 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; 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 1ca630cfa8a6..0a61ee7300dd 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 60277740daa2..96b09135722d 100644 --- a/server/src/test/java/com/cloud/network/as/AutoScaleManagerImplTest.java +++ b/server/src/test/java/com/cloud/network/as/AutoScaleManagerImplTest.java @@ -27,6 +27,7 @@ import com.cloud.agent.api.to.LoadBalancerTO.AutoScaleVmProfileTO; import com.cloud.agent.api.to.LoadBalancerTO.ConditionTO; import com.cloud.agent.api.to.LoadBalancerTO.CounterTO; +import com.cloud.api.ApiDBUtils; import com.cloud.api.dispatch.DispatchChain; import com.cloud.api.dispatch.DispatchChainFactory; import com.cloud.dc.DataCenter; @@ -103,6 +104,8 @@ import com.cloud.vm.dao.DomainRouterDao; import com.cloud.vm.dao.UserVmDao; import com.cloud.vm.dao.VMInstanceDao; +import org.apache.cloudstack.acl.ApiKeyPairVO; +import org.apache.cloudstack.acl.dao.ApiKeyPairDao; import org.apache.cloudstack.affinity.AffinityGroupVO; import org.apache.cloudstack.affinity.dao.AffinityGroupDao; import org.apache.cloudstack.annotation.AnnotationService; @@ -295,9 +298,6 @@ public class AutoScaleManagerImplTest { private static final Map> otherDeployParams = new HashMap<>(); private static final Map> counterParamList = new HashMap<>(); private static final Integer expungeVmGracePeriod = 33; - private static final String cloudStackApiUrl = "cloudstack url"; - private static final String autoScaleUserApiKey = "cloudstack api key"; - private static final String autoScaleUserSecretKey = "cloudstack secret key"; private static final String vmName = "vm name"; private static final String networkUuid = "1111-1111-1116"; private static final Long vmProfileId = 23L; @@ -388,6 +388,9 @@ public class AutoScaleManagerImplTest { DomainRouterVO domainRouterMock; @Mock LoadBalancerVMMapVO loadBalancerVMMapMock; + @Mock + ApiKeyPairDao apiKeyPairDaoMock; + MockedStatic apiDBUtilsMocked; @Before public void setUp() { @@ -411,11 +414,20 @@ public void setUp() { userDataDetails.put("0", new HashMap<>() {{ put("key1", "value1"); put("key2", "value2"); }}); Mockito.doReturn(userDataFinal).when(userVmMgr).finalizeUserData(any(), any(), any()); Mockito.doReturn(userDataFinal).when(userDataMgr).validateUserData(eq(userDataFinal), nullable(BaseCmd.HTTPMethod.class)); + + ApiKeyPairVO apiKeyPairVO = new ApiKeyPairVO(1L, 1L); + apiKeyPairVO.setApiKey("apikey"); + apiKeyPairVO.setSecretKey("secretkey"); + apiDBUtilsMocked = Mockito.mockStatic(ApiDBUtils.class); + apiDBUtilsMocked.when(ReflectionTestUtils.invokeMethod(ApiDBUtils.class, "searchForLatestUserKeyPair", Mockito.anyLong())).thenReturn(apiKeyPairVO); } @After public void tearDown() { CallContext.unregister(); + if (apiDBUtilsMocked != null) { + apiDBUtilsMocked.close(); + } } @Test @@ -862,8 +874,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); @@ -875,9 +885,7 @@ 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); + when(userMock.getAccountId()).thenReturn(accountId + 1L); autoScaleManagerImplSpy.checkAutoScaleUser(autoScaleUserId, accountId); } @@ -885,8 +893,7 @@ public void testCheckAutoScaleUserFail1() { @Test(expected = InvalidParameterValueException.class) public void testCheckAutoScaleUserFail2() { when(userDao.findById(any())).thenReturn(userMock); - when(userMock.getAccountId()).thenReturn(accountId); - when(userMock.getApiKey()).thenReturn(null); + when(userMock.getAccountId()).thenReturn(accountId + 1); autoScaleManagerImplSpy.checkAutoScaleUser(autoScaleUserId, accountId); } @@ -910,8 +917,6 @@ public void testCheckAutoScaleUserFail4() { public void testCheckAutoScaleUserFail5() 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 b24ff6f820ac..4c8ec7d64c05 100644 --- a/server/src/test/java/com/cloud/user/AccountManagerImplTest.java +++ b/server/src/test/java/com/cloud/user/AccountManagerImplTest.java @@ -37,7 +37,6 @@ import org.apache.cloudstack.acl.RolePermissionVO; import org.apache.cloudstack.acl.RoleService; import org.apache.cloudstack.acl.RoleVO; -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.ApiKeyPairService; @@ -177,7 +176,6 @@ public void beforeTest() { keyPair.setId(1L); return keyPair; }); - } @Test @@ -326,10 +324,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(listUserKeysCmd.getKeyId()).thenReturn(null); - Mockito.when(listUserKeysCmd.getUserId()).thenReturn(1L); - Mockito.doThrow(new PermissionDeniedException("PDE")).when(apiKeyPairService).validateCallingUserHasAccessToDesiredUser(Mockito.anyLong()); - Mockito.lenient().when(accountManagerImpl.getAccount(Mockito.anyLong())).thenReturn(accountMock); // Queried account - admin 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); @@ -337,7 +334,7 @@ public void testgetUserCmd() { Mockito.lenient().when(accountService.isNormalUser(Mockito.anyLong())).thenReturn(Boolean.TRUE); Mockito.lenient().when(accountMock.getAccountId()).thenReturn(2L); - accountManagerImpl.getKeys(listUserKeysCmd); + accountManagerImpl.getKeys(_getkeyscmd); } @Test diff --git a/server/src/test/java/com/cloud/user/MockAccountManagerImpl.java b/server/src/test/java/com/cloud/user/MockAccountManagerImpl.java index 3ef91f7c05de..adab88c15a6d 100644 --- a/server/src/test/java/com/cloud/user/MockAccountManagerImpl.java +++ b/server/src/test/java/com/cloud/user/MockAccountManagerImpl.java @@ -28,7 +28,6 @@ import com.cloud.offering.NetworkOffering; import com.cloud.offering.ServiceOffering; import com.cloud.projects.Project.ListProjectResourcesCriteria; -import com.cloud.utils.Pair; import com.cloud.utils.Ternary; import com.cloud.utils.component.Manager; import com.cloud.utils.component.ManagerBase; From 7bfdfb4a92884970876a3acb672bfc841dfab21c Mon Sep 17 00:00:00 2001 From: "klaus.freitas.scclouds" Date: Wed, 31 Jul 2024 14:23:05 -0300 Subject: [PATCH 03/22] small fixes and deleting apikeys on erasure of user and account --- .../apache/cloudstack/acl/dao/ApiKeyPairDao.java | 2 +- .../cloudstack/acl/dao/ApiKeyPairDaoImpl.java | 4 ++-- .../java/com/cloud/user/AccountManagerImpl.java | 13 ++++++++++++- .../cloudstack/acl/ApiKeyPairManagerImpl.java | 2 +- 4 files changed, 16 insertions(+), 5 deletions(-) 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 index 0fb72d920c82..2619b457dea1 100644 --- 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 @@ -30,7 +30,7 @@ public interface ApiKeyPairDao extends GenericDao { ApiKeyPairVO findByUuid(String uuid); - Pair, Integer> listApiKeysByUserOrId(Long userId, Long id); + Pair, Integer> listApiKeysByUserOrApiKeyId(Long userId, Long apiKeyId); ApiKeyPairVO getLastApiKeyCreatedByUser(Long userId); 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 ec14009383d3..e1afa4edcbe6 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 @@ -57,12 +57,12 @@ public ApiKeyPairVO findBySecretKey(String secretKey) { return findOneBy(sc); } - public Pair, Integer> listApiKeysByUserOrId(Long userId, Long id) { + public Pair, Integer> listApiKeysByUserOrApiKeyId(Long userId, Long apiKeyId) { SearchCriteria sc = keyPairSearch.create(); if (userId != null) { sc.setParametersIfNotNull("userId", String.valueOf(userId)); } - sc.setParametersIfNotNull("id", id); + sc.setParametersIfNotNull("id", apiKeyId); final Filter searchFilter = new Filter(100); return searchAndCount(sc, searchFilter); } diff --git a/server/src/main/java/com/cloud/user/AccountManagerImpl.java b/server/src/main/java/com/cloud/user/AccountManagerImpl.java index 96c6b70b8946..09687f260133 100644 --- a/server/src/main/java/com/cloud/user/AccountManagerImpl.java +++ b/server/src/main/java/com/cloud/user/AccountManagerImpl.java @@ -882,6 +882,10 @@ protected boolean cleanupAccount(AccountVO account, long callerUserId, Account c // cleanup the users from the account List users = _userDao.listByAccount(accountId); for (UserVO user : users) { + // remove apikeys + List apiKeyPairs = apiKeyPairDao.listApiKeysByUserOrApiKeyId(user.getId(), null).first(); + apiKeyPairs.stream().forEach(keyPair -> apiKeyPairService.deleteApiKey(keyPair)); + if (!_userDao.remove(user.getId())) { logger.error("Unable to delete user: " + user + " as a part of account " + account + " cleanup"); accountCleanupNeeded = true; @@ -2135,6 +2139,10 @@ public boolean deleteUser(DeleteUserCmd deleteUserCmd) { // don't allow to delete the user from the account of type Project checkAccountAndAccess(user, account); + // remove apikeys + List apiKeyPairs = apiKeyPairDao.listApiKeysByUserOrApiKeyId(id, null).first(); + apiKeyPairs.stream().forEach(keyPair -> apiKeyPairService.deleteApiKey(keyPair)); + return _userDao.remove(id); } @@ -2837,6 +2845,9 @@ private void fetchOnlyOneKeypair(List responses, ListUserKey } else { keyPair = _accountService.getLatestUserKeyPair(CallContext.current().getCallingUser().getId()); } + if (keyPair == null) { + throw new InvalidParameterValueException("No api key was found with the parameters specified"); + } apiKeyPairService.validateCallingUserHasAccessToDesiredUser(keyPair.getUserId()); addKeypairResponse(keyPair, responses, cmd); } @@ -2848,7 +2859,7 @@ private Integer fetchMultipleKeypairs(List responses, ListUs users = List.of(cmd.getUserId()); } else { User callerUser = CallContext.current().getCallingUser(); - users = isRootAdmin(callerUser.getAccountId()) ? queryService.searchForAccessableUsers() : List.of(callerUser.getId()); + users = isAdmin(callerUser.getAccountId()) ? queryService.searchForAccessableUsers() : List.of(callerUser.getId()); } Pair, Integer> keyPairs = apiKeyPairDao.listByUserIdsPaginated(users, cmd); 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 cc354ac6d822..f2674873190b 100644 --- a/server/src/main/java/org/apache/cloudstack/acl/ApiKeyPairManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/acl/ApiKeyPairManagerImpl.java @@ -98,7 +98,7 @@ public void validateCallingUserHasAccessToDesiredUser(Long userId) { } User callerUser = CallContext.current().getCallingUser(); if (!accountManager.isAdmin(callerUser.getAccountId()) && callerUser.getId() != userId) { - throw new PermissionDeniedException("Only root admin can operate on API keys owned by other users"); + throw new PermissionDeniedException("Only admins can operate on API keys owned by other users"); } } } From ac14de202c015bc5472d45111fe67bd5e966303e Mon Sep 17 00:00:00 2001 From: "klaus.freitas.scclouds" Date: Wed, 31 Jul 2024 15:29:44 -0300 Subject: [PATCH 04/22] fixing NPE on user edition authored-by: hsato03 --- .../cloudstack/acl/dao/ApiKeyPairDaoImpl.java | 1 - .../com/cloud/user/AccountManagerImpl.java | 22 ++++++++++++++----- 2 files changed, 17 insertions(+), 6 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 e1afa4edcbe6..5b670a942bbe 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 @@ -95,7 +95,6 @@ public Pair, Integer> listByUserIdsPaginated(List userI return apiKeyPairVOList; } - @Override public boolean update(Long id, ApiKeyPairVO apiKeyPair) { ApiKeyPairVO ub = createForUpdate(); diff --git a/server/src/main/java/com/cloud/user/AccountManagerImpl.java b/server/src/main/java/com/cloud/user/AccountManagerImpl.java index 09687f260133..020a68f56d93 100644 --- a/server/src/main/java/com/cloud/user/AccountManagerImpl.java +++ b/server/src/main/java/com/cloud/user/AccountManagerImpl.java @@ -1680,17 +1680,29 @@ protected ApiKeyPairVO validateAndUpdateApiAndSecretKeyIfNeeded(UpdateUserCmd up if (isApiKeyBlank && isSecretKeyBlank) { return null; } + Ternary keyPairTernary = _accountDao.findUserAccountByApiKey(apiKey); - if (keyPairTernary != null) { - 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)); - } + + 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 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; + } /** From 16fe2f00aa89583cbc9b4e895d792058fe6615a0 Mon Sep 17 00:00:00 2001 From: "klaus.freitas.scclouds" Date: Wed, 31 Jul 2024 16:34:10 -0300 Subject: [PATCH 05/22] small tweaks --- .../main/java/org/apache/cloudstack/acl/ApiKeyPairVO.java | 6 +++++- server/src/main/java/com/cloud/user/AccountManagerImpl.java | 4 ---- 2 files changed, 5 insertions(+), 5 deletions(-) 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 index 2b65982b074a..6b1ba118ec84 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/acl/ApiKeyPairVO.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/acl/ApiKeyPairVO.java @@ -101,7 +101,11 @@ public ApiKeyPairVO(Long userId, String description, Date startDate, Date endDat } public ApiKeyPairVO(String name, Long userId, String description, Date startDate, Date endDate, Account account) { - this.name = name; + if (name == null) { + this.name = userId + " - API Keypair"; + } else { + this.name = name; + } this.userId = userId; this.description = description; this.startDate = startDate; diff --git a/server/src/main/java/com/cloud/user/AccountManagerImpl.java b/server/src/main/java/com/cloud/user/AccountManagerImpl.java index 020a68f56d93..f3c3eca211c6 100644 --- a/server/src/main/java/com/cloud/user/AccountManagerImpl.java +++ b/server/src/main/java/com/cloud/user/AccountManagerImpl.java @@ -3059,10 +3059,6 @@ private String createUserSecretKey(long userId, ApiKeyPairVO newApiKeyPair) { */ @DB private ApiKeyPairVO validateAndPersistKeyPairAndPermissions(Account account, ApiKeyPairVO newApiKeyPair, List> rules, Boolean accessedByApiKey, String apiKey) { - if (newApiKeyPair.getName() == null) { - User user = _userDao.findById(newApiKeyPair.getUserId()); - newApiKeyPair.setName(user.getUsername() + " - API Keypair"); - } final ApiKeyPairVO savedApiKeyPair = apiKeyPairDao.persist(newApiKeyPair); final Role accountRole = roleService.findRole(account.getRoleId()); From 378fc4fd56700d2e2cb66ea83b7cb3ac44e05d24 Mon Sep 17 00:00:00 2001 From: "klaus.freitas.scclouds" Date: Wed, 31 Jul 2024 17:51:09 -0300 Subject: [PATCH 06/22] fix tests --- .../src/test/java/com/cloud/user/AccountManagerImplTest.java | 5 ++++- 1 file changed, 4 insertions(+), 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 4c8ec7d64c05..7a497cf91dc1 100644 --- a/server/src/test/java/com/cloud/user/AccountManagerImplTest.java +++ b/server/src/test/java/com/cloud/user/AccountManagerImplTest.java @@ -155,7 +155,6 @@ public class AccountManagerImplTest extends AccountManagetImplTestBase { @Mock RegisterCmd registerCmdMock; - @Before public void setUp() throws Exception { enableUserTwoFactorAuthenticationMock = Mockito.mock(ConfigKey.class); @@ -176,6 +175,9 @@ public void beforeTest() { keyPair.setId(1L); return keyPair; }); + + Pair, Integer> pair = new Pair(List.of(), 0); + Mockito.when(keyPairDaoMock.listApiKeysByUserOrApiKeyId(Mockito.any(), Mockito.any())).thenReturn(pair); } @Test @@ -454,6 +456,7 @@ public void validateAndUpdatApiAndSecretKeyIfNeededTest() { Ternary pairUserAccountMock = new Ternary<>(otherUserMock, Mockito.mock(Account.class), apiKeyPairVOMock); Mockito.doReturn(pairUserAccountMock).when(_accountDao).findUserAccountByApiKey(apiKey); + Mockito.when(_accountDao.findById(Mockito.anyLong())).thenReturn(accountVoMock); accountManagerImpl.validateAndUpdateApiAndSecretKeyIfNeeded(UpdateUserCmdMock, userVoMock); Mockito.verify(_accountDao).findUserAccountByApiKey(apiKey); From a7057c5f031acaaeab8d4dee9a961f313b8b2cbc Mon Sep 17 00:00:00 2001 From: "klaus.freitas.scclouds" Date: Mon, 12 Aug 2024 17:07:23 -0300 Subject: [PATCH 07/22] fixing mistake on test --- .../src/test/java/com/cloud/user/AccountManagerImplTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/test/java/com/cloud/user/AccountManagerImplTest.java b/server/src/test/java/com/cloud/user/AccountManagerImplTest.java index 65465343cf7d..356cd9fd65a7 100644 --- a/server/src/test/java/com/cloud/user/AccountManagerImplTest.java +++ b/server/src/test/java/com/cloud/user/AccountManagerImplTest.java @@ -344,7 +344,7 @@ public void testgetUserCmd() { @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); @@ -368,7 +368,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 From 989b4c310ec02ca3af208ce8a2c4218b1d27b52d Mon Sep 17 00:00:00 2001 From: "klaus.freitas.scclouds" Date: Wed, 14 Aug 2024 14:30:35 -0300 Subject: [PATCH 08/22] fixing compilation failure --- .../network/contrail/management/MockAccountManager.java | 6 +++--- server/src/main/java/com/cloud/user/AccountManager.java | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) 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 610f4aa82aac..8b035c5fa19e 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 @@ -118,7 +118,7 @@ public void checkAccess(Account arg0, AccessType arg1, boolean arg2, ControlledE } @Override - public String[] createApiKeyAndSecretKey(RegisterCmd arg0) { + public ApiKeyPair createApiKeyAndSecretKey(RegisterCmd cmd) { // TODO Auto-generated method stub return null; } @@ -394,7 +394,7 @@ public UserAccount enableUser(long arg0) { } @Override - public Pair findUserByApiKey(String arg0) { + public Ternary findUserByApiKey(String arg0) { // TODO Auto-generated method stub return null; } @@ -486,7 +486,7 @@ public Map getKeys(GetUserKeysCmd cmd){ } @Override - public Map getKeys(Long userId) { + public ListResponse getKeys(ListUserKeysCmd cmd) { return null; } diff --git a/server/src/main/java/com/cloud/user/AccountManager.java b/server/src/main/java/com/cloud/user/AccountManager.java index f50b87c7ac74..b20eaf25f0ed 100644 --- a/server/src/main/java/com/cloud/user/AccountManager.java +++ b/server/src/main/java/com/cloud/user/AccountManager.java @@ -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 */ - public Ternary findUserByApiKey(String apiKey); + Ternary findUserByApiKey(String apiKey); boolean enableAccount(long accountId); From b0ba748c9d5bc17c12bf5ee4a300f8771c397cc7 Mon Sep 17 00:00:00 2001 From: "klaus.freitas.scclouds" Date: Mon, 19 Aug 2024 12:22:35 -0300 Subject: [PATCH 09/22] fix checkstyles --- .../network/contrail/management/MockAccountManager.java | 1 - 1 file changed, 1 deletion(-) 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 8b035c5fa19e..93e39027b937 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 @@ -62,7 +62,6 @@ import com.cloud.user.UserVO; import com.cloud.user.dao.AccountDao; import com.cloud.user.dao.UserDao; -import com.cloud.utils.Pair; import com.cloud.utils.Ternary; import com.cloud.utils.component.ManagerBase; import com.cloud.utils.db.SearchBuilder; From 83bdb682fd4c87c75b7bac1d00ae24f722fcc67c Mon Sep 17 00:00:00 2001 From: "klaus.freitas.scclouds" Date: Tue, 20 Aug 2024 09:32:45 -0300 Subject: [PATCH 10/22] fix version --- .../cloudstack/api/command/admin/user/DeleteUserKeysCmd.java | 2 +- .../cloudstack/api/command/admin/user/ListUserKeyRulesCmd.java | 2 +- .../cloudstack/api/command/admin/user/ListUserKeysCmd.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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 index b0f19341b708..7da6201aad71 100644 --- 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 @@ -30,7 +30,7 @@ import org.apache.cloudstack.api.response.SuccessResponse; @APICommand(name = "deleteUserKeys", description = "Deletes a keypair from a user", responseObject = SuccessResponse.class, - since = "4.18.0.11-scclouds", requestHasSensitiveInfo = false, responseHasSensitiveInfo = false) + since = "4.20.0", requestHasSensitiveInfo = false, responseHasSensitiveInfo = false) public class DeleteUserKeysCmd extends BaseAsyncCmd { @ACL 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 index 2b111264de8d..0d86f78b6b81 100644 --- 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 @@ -40,7 +40,7 @@ responseObject = BaseRolePermissionResponse.class, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, - since = "4.18.0.11-scclouds") + since = "4.20.0") public class ListUserKeyRulesCmd extends BaseCmd { 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 index 0790f6418ee2..b577250de68c 100644 --- 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 @@ -40,7 +40,7 @@ requestHasSensitiveInfo = false, responseHasSensitiveInfo = true, authorized = {RoleType.User, RoleType.Admin, RoleType.DomainAdmin, RoleType.ResourceAdmin}, - since = "4.10.0") + since = "4.20.0") public class ListUserKeysCmd extends BaseListDomainResourcesCmd { From 33ae5189cfdc1f8214b22d84a5a0c24b3205f580 Mon Sep 17 00:00:00 2001 From: "klaus.freitas.scclouds" Date: Tue, 20 Aug 2024 09:39:43 -0300 Subject: [PATCH 11/22] fix lint, ty Dahn --- .../cloudstack/api/command/admin/user/ListUserKeysCmd.java | 2 +- .../java/org/apache/cloudstack/acl/ApiKeyPairPermissionVO.java | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) 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 index b577250de68c..d1fdcaa61d4c 100644 --- 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 @@ -83,4 +83,4 @@ public void execute() { finalResponse.setResponseName(getCommandName()); setResponseObject(finalResponse); } -} \ No newline at end of file +} 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 84fbb82c19ca..bd8a6d471db3 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 @@ -51,4 +51,3 @@ public long getSortOrder() { return sortOrder; } } - From 2b910d028149726220333add97d1b55d9fed842a Mon Sep 17 00:00:00 2001 From: "klaus.freitas.scclouds" Date: Tue, 20 Aug 2024 17:00:41 -0300 Subject: [PATCH 12/22] fix another compilation mistake, it compiles locally --- .../com/cloud/hypervisor/kvm/storage/LinstorStorageAdaptor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/storage/volume/linstor/src/main/java/com/cloud/hypervisor/kvm/storage/LinstorStorageAdaptor.java b/plugins/storage/volume/linstor/src/main/java/com/cloud/hypervisor/kvm/storage/LinstorStorageAdaptor.java index 971c2e8c9fd8..5d037d63433b 100644 --- a/plugins/storage/volume/linstor/src/main/java/com/cloud/hypervisor/kvm/storage/LinstorStorageAdaptor.java +++ b/plugins/storage/volume/linstor/src/main/java/com/cloud/hypervisor/kvm/storage/LinstorStorageAdaptor.java @@ -483,7 +483,7 @@ private boolean resourceSupportZeroBlocks(KVMStoragePool destPool, String resNam } ); } } catch (ApiException apiExc) { - s_logger.error(apiExc.getMessage()); + logger.error(apiExc.getMessage()); } return false; } From a2e9abad7fcae5464bbcbe6b590c4d39aecb3d2e Mon Sep 17 00:00:00 2001 From: "klaus.freitas.scclouds" Date: Wed, 21 Aug 2024 16:11:53 -0300 Subject: [PATCH 13/22] fix building with no redist packages, should pass tests now --- .../contrail/management/MockAccountManager.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) 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 244749b68282..702afecdca43 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 @@ -32,8 +32,12 @@ 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.command.admin.user.ListUserKeysCmd; +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; @@ -489,6 +493,16 @@ public ListResponse getKeys(ListUserKeysCmd cmd) { return null; } + @Override + public ApiKeyPair getKeyPairById(Long id) { + return null; + } + + @Override + public ApiKeyPair getLatestUserKeyPair(Long id) { + return null; + } + @Override public List listUserTwoFactorAuthenticationProviders() { return null; From b40e777f7ad7ebb8aea8e8228d539919b3bf8b20 Mon Sep 17 00:00:00 2001 From: "klaus.freitas.scclouds" Date: Wed, 28 Aug 2024 14:45:58 -0300 Subject: [PATCH 14/22] refactoring RegisterCmd to RegisterUserKeysCmd and userid to id in listUserKeys api --- .../java/com/cloud/user/AccountService.java | 4 +- .../command/admin/user/ListUserKeysCmd.java | 2 +- ...isterCmd.java => RegisterUserKeysCmd.java} | 2 +- .../cloud/server/ManagementServerImpl.java | 4 +- .../com/cloud/user/AccountManagerImpl.java | 4 +- .../cloud/user/AccountManagerImplTest.java | 88 +++++++++---------- .../cloud/user/MockAccountManagerImpl.java | 4 +- 7 files changed, 54 insertions(+), 54 deletions(-) rename api/src/main/java/org/apache/cloudstack/api/command/admin/user/{RegisterCmd.java => RegisterUserKeysCmd.java} (99%) diff --git a/api/src/main/java/com/cloud/user/AccountService.java b/api/src/main/java/com/cloud/user/AccountService.java index d6d4faceed8a..c692235f66bb 100644 --- a/api/src/main/java/com/cloud/user/AccountService.java +++ b/api/src/main/java/com/cloud/user/AccountService.java @@ -26,7 +26,7 @@ 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.ListUserKeysCmd; -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 com.cloud.dc.DataCenter; @@ -96,7 +96,7 @@ User createUser(String userName, String password, String firstName, String lastN void markUserRegistered(long userId); - public ApiKeyPair createApiKeyAndSecretKey(RegisterCmd cmd); + public ApiKeyPair createApiKeyAndSecretKey(RegisterUserKeysCmd cmd); public String[] createApiKeyAndSecretKey(final long userId); 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 index d1fdcaa61d4c..19bf4e3cd380 100644 --- 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 @@ -45,7 +45,7 @@ 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.") + @Parameter(name=ApiConstants.ID, type = CommandType.UUID, entityType = UserResponse.class, description = "ID of the user that owns the keys.") private Long userId; @ACL 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/RegisterUserKeysCmd.java similarity index 99% rename from api/src/main/java/org/apache/cloudstack/api/command/admin/user/RegisterCmd.java rename to api/src/main/java/org/apache/cloudstack/api/command/admin/user/RegisterUserKeysCmd.java index 54801f748df5..44715341aa95 100644 --- 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/RegisterUserKeysCmd.java @@ -46,7 +46,7 @@ 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 RegisterCmd extends BaseCmd { +public class RegisterUserKeysCmd extends BaseCmd { protected Logger logger = LogManager.getLogger(getClass()); @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = UserResponse.class, required = true, description = "User ID.") diff --git a/server/src/main/java/com/cloud/server/ManagementServerImpl.java b/server/src/main/java/com/cloud/server/ManagementServerImpl.java index 74c66d1ba5a4..b25ab8b403e7 100644 --- a/server/src/main/java/com/cloud/server/ManagementServerImpl.java +++ b/server/src/main/java/com/cloud/server/ManagementServerImpl.java @@ -280,7 +280,7 @@ 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; @@ -3612,7 +3612,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); diff --git a/server/src/main/java/com/cloud/user/AccountManagerImpl.java b/server/src/main/java/com/cloud/user/AccountManagerImpl.java index 49990ac91016..b50a782ea832 100644 --- a/server/src/main/java/com/cloud/user/AccountManagerImpl.java +++ b/server/src/main/java/com/cloud/user/AccountManagerImpl.java @@ -71,7 +71,7 @@ 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.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.ListResponse; @@ -2940,7 +2940,7 @@ public UserTwoFactorAuthenticator getUserTwoFactorAuthenticationProvider(final S @Override @DB @ActionEvent(eventType = EventTypes.EVENT_REGISTER_FOR_SECRET_API_KEY, eventDescription = "register for the developer API keys") - public ApiKeyPair createApiKeyAndSecretKey(RegisterCmd cmd) { + public ApiKeyPair createApiKeyAndSecretKey(RegisterUserKeysCmd cmd) { Account caller = getCurrentCallingAccount(); User user = _userDao.findById(cmd.getUserId()); if (user == null) { diff --git a/server/src/test/java/com/cloud/user/AccountManagerImplTest.java b/server/src/test/java/com/cloud/user/AccountManagerImplTest.java index 356cd9fd65a7..d1113dd0b5a5 100644 --- a/server/src/test/java/com/cloud/user/AccountManagerImplTest.java +++ b/server/src/test/java/com/cloud/user/AccountManagerImplTest.java @@ -48,7 +48,7 @@ import org.apache.cloudstack.acl.ControlledEntity; 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.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.UserTwoFactorAuthenticationSetupResponse; import org.apache.cloudstack.auth.UserAuthenticator; @@ -155,7 +155,7 @@ public class AccountManagerImplTest extends AccountManagetImplTestBase { RoleService roleServiceMock; @Mock - RegisterCmd registerCmdMock; + RegisterUserKeysCmd registerUserKeysCmdMock; @Before public void setUp() throws Exception { @@ -1173,8 +1173,8 @@ public void createApiKeyAndSecretKeyTestWithEmptyRules() { 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(registerUserKeysCmdMock.getRules()).thenReturn(List.of()); + Mockito.when(registerUserKeysCmdMock.getUserId()).thenReturn(userId); Mockito.when(userDaoMock.findById(any())).thenReturn(userVoMock); Mockito.when(_accountDao.findById(Mockito.anyLong())).thenReturn(accountVoMock); @@ -1185,7 +1185,7 @@ public void createApiKeyAndSecretKeyTestWithEmptyRules() { )); Mockito.when(roleServiceMock.findRole(Mockito.anyLong())).thenReturn(new RoleVO()); - accountManagerImpl.createApiKeyAndSecretKey(registerCmdMock); + accountManagerImpl.createApiKeyAndSecretKey(registerUserKeysCmdMock); Mockito.verify(keyPairDaoMock, Mockito.times(0)).remove(Mockito.anyLong()); Mockito.verify(keyPairPermissionsDaoMock, Mockito.times(1)).persist(Mockito.any(ApiKeyPairPermissionVO.class)); } @@ -1202,8 +1202,8 @@ public void createApiKeyAndSecretKeyTestPermissionNotPresentOnAccount() { ApiConstants.PERMISSION, RolePermission.Permission.ALLOW, ApiConstants.DESCRIPTION, "description" )); - Mockito.when(registerCmdMock.getRules()).thenReturn(rules); - Mockito.when(registerCmdMock.getUserId()).thenReturn(userId); + Mockito.when(registerUserKeysCmdMock.getRules()).thenReturn(rules); + Mockito.when(registerUserKeysCmdMock.getUserId()).thenReturn(userId); ApiKeyPairVO apiKeyPairVO = new ApiKeyPairVO(); apiKeyPairVO.setUserId(userId); @@ -1217,7 +1217,7 @@ public void createApiKeyAndSecretKeyTestPermissionNotPresentOnAccount() { )); Mockito.when(roleServiceMock.findRole(Mockito.anyLong())).thenReturn(new RoleVO()); - accountManagerImpl.createApiKeyAndSecretKey(registerCmdMock); + accountManagerImpl.createApiKeyAndSecretKey(registerUserKeysCmdMock); Mockito.verify(keyPairDaoMock, Mockito.times(1)).remove(Mockito.anyLong()); Mockito.verify(keyPairPermissionsDaoMock, Mockito.times(0)).persist(Mockito.any(ApiKeyPairPermissionVO.class)); } @@ -1234,8 +1234,8 @@ public void createApiKeyAndSecretTestKeyDeniedOnAccount() { ApiConstants.PERMISSION, RolePermission.Permission.ALLOW, ApiConstants.DESCRIPTION, "description" )); - Mockito.when(registerCmdMock.getRules()).thenReturn(rules); - Mockito.when(registerCmdMock.getUserId()).thenReturn(userId); + Mockito.when(registerUserKeysCmdMock.getRules()).thenReturn(rules); + Mockito.when(registerUserKeysCmdMock.getUserId()).thenReturn(userId); ApiKeyPairVO apiKeyPairVO = new ApiKeyPairVO(); apiKeyPairVO.setUserId(userId); @@ -1250,7 +1250,7 @@ public void createApiKeyAndSecretTestKeyDeniedOnAccount() { )); Mockito.when(roleServiceMock.findRole(Mockito.anyLong())).thenReturn(new RoleVO()); - accountManagerImpl.createApiKeyAndSecretKey(registerCmdMock); + accountManagerImpl.createApiKeyAndSecretKey(registerUserKeysCmdMock); Mockito.verify(keyPairDaoMock, Mockito.times(1)).remove(Mockito.anyLong()); Mockito.verify(keyPairPermissionsDaoMock, Mockito.times(0)).persist(Mockito.any(ApiKeyPairPermissionVO.class)); } @@ -1267,8 +1267,8 @@ public void createApiKeyAndSecretKeyTestAllowedOnAccount() { ApiConstants.PERMISSION, RolePermission.Permission.ALLOW, ApiConstants.DESCRIPTION, "description" )); - Mockito.when(registerCmdMock.getRules()).thenReturn(rules); - Mockito.when(registerCmdMock.getUserId()).thenReturn(userId); + Mockito.when(registerUserKeysCmdMock.getRules()).thenReturn(rules); + Mockito.when(registerUserKeysCmdMock.getUserId()).thenReturn(userId); UserVO mockUser = new UserVO(userId); @@ -1287,7 +1287,7 @@ public void createApiKeyAndSecretKeyTestAllowedOnAccount() { )); Mockito.when(roleServiceMock.findRole(Mockito.anyLong())).thenReturn(new RoleVO()); - Assert.assertEquals((long) accountManagerImpl.createApiKeyAndSecretKey(registerCmdMock).getUserId(), userId); + Assert.assertEquals((long) accountManagerImpl.createApiKeyAndSecretKey(registerUserKeysCmdMock).getUserId(), userId); Mockito.verify(keyPairPermissionsDaoMock, Mockito.times(1)).persist(Mockito.any(ApiKeyPairPermissionVO.class)); } @@ -1303,8 +1303,8 @@ public void createApiKeyAndSecretKeyTestWithDeniedThatIsAllowedOnAccount() { ApiConstants.PERMISSION, RolePermission.Permission.ALLOW, ApiConstants.DESCRIPTION, "description" )); - Mockito.when(registerCmdMock.getRules()).thenReturn(rules); - Mockito.when(registerCmdMock.getUserId()).thenReturn(userId); + Mockito.when(registerUserKeysCmdMock.getRules()).thenReturn(rules); + Mockito.when(registerUserKeysCmdMock.getUserId()).thenReturn(userId); UserVO mockUser = new UserVO(userId); ApiKeyPairPermissionVO permissionVO = new ApiKeyPairPermissionVO(); @@ -1322,7 +1322,7 @@ public void createApiKeyAndSecretKeyTestWithDeniedThatIsAllowedOnAccount() { )); Mockito.when(roleServiceMock.findRole(Mockito.anyLong())).thenReturn(new RoleVO()); - ApiKeyPair keyPairVO = accountManagerImpl.createApiKeyAndSecretKey(registerCmdMock); + ApiKeyPair keyPairVO = accountManagerImpl.createApiKeyAndSecretKey(registerUserKeysCmdMock); Assert.assertEquals((long) keyPairVO.getUserId(), userId); } @@ -1341,11 +1341,11 @@ public void createApiAndSecretKeyTestWithNonEmptyDates() { 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"); + Mockito.when(registerUserKeysCmdMock.getRules()).thenReturn(rules); + Mockito.when(registerUserKeysCmdMock.getUserId()).thenReturn(userId); + Mockito.when(registerUserKeysCmdMock.getStartDate()).thenReturn(startDate); + Mockito.when(registerUserKeysCmdMock.getEndDate()).thenReturn(endDate); + Mockito.when(registerUserKeysCmdMock.getDescription()).thenReturn("key description"); UserVO mockUser = new UserVO(userId); ApiKeyPairPermissionVO permissionVO = new ApiKeyPairPermissionVO(); @@ -1363,7 +1363,7 @@ public void createApiAndSecretKeyTestWithNonEmptyDates() { )); Mockito.when(roleServiceMock.findRole(Mockito.anyLong())).thenReturn(new RoleVO()); - ApiKeyPair response = accountManagerImpl.createApiKeyAndSecretKey(registerCmdMock); + ApiKeyPair response = accountManagerImpl.createApiKeyAndSecretKey(registerUserKeysCmdMock); Mockito.verify(keyPairDaoMock, Mockito.times(1)).persist(Mockito.any(ApiKeyPairVO.class)); Assert.assertEquals((long) response.getUserId(), userId); Assert.assertEquals(response.getStartDate(), startDate); @@ -1386,11 +1386,11 @@ public void createApiAndSecretKeyTestWithExpiredDate() { 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"); + Mockito.when(registerUserKeysCmdMock.getRules()).thenReturn(rules); + Mockito.when(registerUserKeysCmdMock.getUserId()).thenReturn(userId); + Mockito.when(registerUserKeysCmdMock.getStartDate()).thenReturn(startDate); + Mockito.when(registerUserKeysCmdMock.getEndDate()).thenReturn(endDate); + Mockito.when(registerUserKeysCmdMock.getDescription()).thenReturn("key description"); UserVO mockUser = new UserVO(userId); ApiKeyPairPermissionVO permissionVO = new ApiKeyPairPermissionVO(); @@ -1402,7 +1402,7 @@ public void createApiAndSecretKeyTestWithExpiredDate() { 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); + ApiKeyPair response = accountManagerImpl.createApiKeyAndSecretKey(registerUserKeysCmdMock); Assert.assertEquals((long) response.getUserId(), userId); Assert.assertEquals(response.getStartDate(), startDate); Assert.assertEquals(response.getEndDate(), endDate); @@ -1424,11 +1424,11 @@ public void createApiAndSecretKeyTestWithInvalidDate() { 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"); + Mockito.when(registerUserKeysCmdMock.getRules()).thenReturn(rules); + Mockito.when(registerUserKeysCmdMock.getUserId()).thenReturn(userId); + Mockito.when(registerUserKeysCmdMock.getStartDate()).thenReturn(startDate); + Mockito.when(registerUserKeysCmdMock.getEndDate()).thenReturn(endDate); + Mockito.when(registerUserKeysCmdMock.getDescription()).thenReturn("key description"); UserVO mockUser = new UserVO(userId); ApiKeyPairPermissionVO permissionVO = new ApiKeyPairPermissionVO(); @@ -1440,7 +1440,7 @@ public void createApiAndSecretKeyTestWithInvalidDate() { 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); + ApiKeyPair response = accountManagerImpl.createApiKeyAndSecretKey(registerUserKeysCmdMock); Assert.assertEquals((long) response.getUserId(), userId); Assert.assertEquals(response.getStartDate(), startDate); Assert.assertEquals(response.getEndDate(), endDate); @@ -1464,8 +1464,8 @@ public void createApiAndSecretKeyTestWithMultipleAllowedPermissions() { ApiConstants.PERMISSION, RolePermission.Permission.ALLOW, ApiConstants.DESCRIPTION, "description" )); - Mockito.when(registerCmdMock.getRules()).thenReturn(rules); - Mockito.when(registerCmdMock.getUserId()).thenReturn(userId); + Mockito.when(registerUserKeysCmdMock.getRules()).thenReturn(rules); + Mockito.when(registerUserKeysCmdMock.getUserId()).thenReturn(userId); UserVO mockUser = new UserVO(userId); ApiKeyPairPermissionVO permissionVO = new ApiKeyPairPermissionVO(); @@ -1484,7 +1484,7 @@ public void createApiAndSecretKeyTestWithMultipleAllowedPermissions() { )); Mockito.when(roleServiceMock.findRole(Mockito.anyLong())).thenReturn(new RoleVO()); - accountManagerImpl.createApiKeyAndSecretKey(registerCmdMock); + accountManagerImpl.createApiKeyAndSecretKey(registerUserKeysCmdMock); Mockito.verify(keyPairDaoMock, Mockito.times(0)).remove(Mockito.anyLong()); Mockito.verify(keyPairPermissionsDaoMock, Mockito.times(2)).persist(Mockito.any(ApiKeyPairPermissionVO.class)); } @@ -1511,8 +1511,8 @@ public void createApiAndSecretKeyTestWithMultipleAllowedPermissionsOneDenied() { ApiConstants.PERMISSION, RolePermission.Permission.ALLOW, ApiConstants.DESCRIPTION, "description" )); - Mockito.when(registerCmdMock.getRules()).thenReturn(rules); - Mockito.when(registerCmdMock.getUserId()).thenReturn(userId); + Mockito.when(registerUserKeysCmdMock.getRules()).thenReturn(rules); + Mockito.when(registerUserKeysCmdMock.getUserId()).thenReturn(userId); UserVO mockUser = new UserVO(userId); ApiKeyPairVO apiKeyPairVO = new ApiKeyPairVO(); @@ -1530,7 +1530,7 @@ public void createApiAndSecretKeyTestWithMultipleAllowedPermissionsOneDenied() { )); Mockito.when(roleServiceMock.findRole(Mockito.anyLong())).thenReturn(new RoleVO()); - accountManagerImpl.createApiKeyAndSecretKey(registerCmdMock); + accountManagerImpl.createApiKeyAndSecretKey(registerUserKeysCmdMock); Mockito.verify(keyPairDaoMock, Mockito.times(1)).remove(Mockito.anyLong()); Mockito.verify(keyPairPermissionsDaoMock, Mockito.times(0)).persist(Mockito.any(ApiKeyPairPermissionVO.class)); } @@ -1557,8 +1557,8 @@ public void createApiAndSecretKeyTestWithMultipleAllowedPermissionsOneDoesNotExi ApiConstants.PERMISSION, RolePermission.Permission.DENY, ApiConstants.DESCRIPTION, "description" )); - Mockito.when(registerCmdMock.getRules()).thenReturn(rules); - Mockito.when(registerCmdMock.getUserId()).thenReturn(userId); + Mockito.when(registerUserKeysCmdMock.getRules()).thenReturn(rules); + Mockito.when(registerUserKeysCmdMock.getUserId()).thenReturn(userId); ApiKeyPairVO apiKeyPairVO = new ApiKeyPairVO(); apiKeyPairVO.setUserId(userId); @@ -1574,7 +1574,7 @@ public void createApiAndSecretKeyTestWithMultipleAllowedPermissionsOneDoesNotExi )); Mockito.when(roleServiceMock.findRole(Mockito.anyLong())).thenReturn(new RoleVO()); - accountManagerImpl.createApiKeyAndSecretKey(registerCmdMock); + accountManagerImpl.createApiKeyAndSecretKey(registerUserKeysCmdMock); Mockito.verify(keyPairDaoMock, Mockito.times(0)).remove(Mockito.anyLong()); Mockito.verify(keyPairPermissionsDaoMock, Mockito.times(0)).persist(Mockito.any(ApiKeyPairPermissionVO.class)); } diff --git a/server/src/test/java/com/cloud/user/MockAccountManagerImpl.java b/server/src/test/java/com/cloud/user/MockAccountManagerImpl.java index 1b2fe54296f3..a8ecfc7effe6 100644 --- a/server/src/test/java/com/cloud/user/MockAccountManagerImpl.java +++ b/server/src/test/java/com/cloud/user/MockAccountManagerImpl.java @@ -43,7 +43,7 @@ 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.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.ListResponse; @@ -314,7 +314,7 @@ public Ternary findUserByApiKey(String apiKey) { } @Override - public ApiKeyPair createApiKeyAndSecretKey(RegisterCmd cmd) { + public ApiKeyPair createApiKeyAndSecretKey(RegisterUserKeysCmd cmd) { return null; } From 95e79fcae385d6b6b01be8b30ee2bc70d5e5d331 Mon Sep 17 00:00:00 2001 From: "klaus.freitas.scclouds" Date: Thu, 29 Aug 2024 10:34:09 -0300 Subject: [PATCH 15/22] fixing one last reference to old RegisterCmd class --- .../network/contrail/management/MockAccountManager.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 702afecdca43..6d8f8380eddb 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 @@ -41,7 +41,7 @@ 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.RegisterUserKeysCmd; import org.apache.cloudstack.api.command.admin.user.UpdateUserCmd; import org.apache.cloudstack.context.CallContext; @@ -121,7 +121,7 @@ public void checkAccess(Account arg0, AccessType arg1, boolean arg2, ControlledE } @Override - public ApiKeyPair createApiKeyAndSecretKey(RegisterCmd cmd) { + public ApiKeyPair createApiKeyAndSecretKey(RegisterUserKeysCmd cmd) { // TODO Auto-generated method stub return null; } From 72a62ed3cec48ad3f23c59885fc552984da1a09c Mon Sep 17 00:00:00 2001 From: "klaus.freitas.scclouds" Date: Fri, 6 Sep 2024 17:24:56 -0300 Subject: [PATCH 16/22] fixing null pointer when calling getUserKeys and the user did not have an API key (fix failing marvin tests) --- .../src/main/java/com/cloud/user/AccountManagerImpl.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/server/src/main/java/com/cloud/user/AccountManagerImpl.java b/server/src/main/java/com/cloud/user/AccountManagerImpl.java index b4f043fb79a4..6b05ff19e2a6 100644 --- a/server/src/main/java/com/cloud/user/AccountManagerImpl.java +++ b/server/src/main/java/com/cloud/user/AccountManagerImpl.java @@ -2839,9 +2839,10 @@ public Map getKeys(GetUserKeysCmd cmd) { Map keys = new HashMap(); ApiKeyPair keyPair = getLatestUserKeyPair(cmd.getID()); - keys.put("apikey", keyPair.getApiKey()); - keys.put("secretkey", keyPair.getSecretKey()); - + if (keyPair != null) { + keys.put("apikey", keyPair.getApiKey()); + keys.put("secretkey", keyPair.getSecretKey()); + } return keys; } From 4591e23e75c40a1171d0bd26fd769ea49bc39165 Mon Sep 17 00:00:00 2001 From: "klaus.freitas.scclouds" Date: Fri, 20 Sep 2024 14:23:24 -0300 Subject: [PATCH 17/22] fixing marvin test --- .../java/org/apache/cloudstack/acl/ApiKeyPairManagerImpl.java | 4 ---- 1 file changed, 4 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 f2674873190b..653094134ded 100644 --- a/server/src/main/java/org/apache/cloudstack/acl/ApiKeyPairManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/acl/ApiKeyPairManagerImpl.java @@ -96,9 +96,5 @@ public void validateCallingUserHasAccessToDesiredUser(Long userId) { throw new InvalidParameterValueException(String.format("Could not perform operation because calling user has less permissions " + "than the informed user [%s].", desiredUser.getId())); } - User callerUser = CallContext.current().getCallingUser(); - if (!accountManager.isAdmin(callerUser.getAccountId()) && callerUser.getId() != userId) { - throw new PermissionDeniedException("Only admins can operate on API keys owned by other users"); - } } } From 03632d520423b25a701263d28e704ac87a32e9f0 Mon Sep 17 00:00:00 2001 From: "klaus.freitas.scclouds" Date: Wed, 25 Sep 2024 10:24:07 -0300 Subject: [PATCH 18/22] fix checkstyles --- .../java/org/apache/cloudstack/acl/ApiKeyPairManagerImpl.java | 1 - 1 file changed, 1 deletion(-) 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 653094134ded..dd1d5bdd0a8a 100644 --- a/server/src/main/java/org/apache/cloudstack/acl/ApiKeyPairManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/acl/ApiKeyPairManagerImpl.java @@ -27,7 +27,6 @@ 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.context.CallContext; import org.apache.cloudstack.query.QueryService; import org.apache.cloudstack.utils.baremetal.BaremetalUtils; From 4817b13582352bf21fa364c24cbffaae494e3c35 Mon Sep 17 00:00:00 2001 From: "klaus.freitas.scclouds" Date: Wed, 16 Oct 2024 16:24:22 -0300 Subject: [PATCH 19/22] fixing asterisk, refactoring apikey permissions check --- .../java/com/cloud/user/AccountService.java | 14 + .../java/com/cloud/user/ApiKeyPairState.java | 21 ++ .../apache/cloudstack/acl/RoleService.java | 19 ++ .../java/org/apache/cloudstack/acl/Rule.java | 6 +- .../cloudstack/acl/apikeypair/ApiKeyPair.java | 4 +- .../acl/apikeypair/ApiKeyPairService.java | 4 +- .../apache/cloudstack/api/ApiConstants.java | 5 + .../command/admin/user/DeleteUserKeysCmd.java | 12 +- .../admin/user/ListUserKeyRulesCmd.java | 15 +- .../command/admin/user/ListUserKeysCmd.java | 20 +- .../admin/user/RegisterUserKeysCmd.java | 6 +- .../api/response/ApiKeyPairResponse.java | 92 +++++-- .../com/cloud/user/dao/AccountDaoImpl.java | 1 + .../acl/ApiKeyPairPermissionVO.java | 4 + .../apache/cloudstack/acl/ApiKeyPairVO.java | 19 +- plugins/api/discovery/pom.xml | 6 + .../command/user/discovery/ListApisCmd.java | 2 +- .../discovery/ApiDiscoveryService.java | 3 +- .../discovery/ApiDiscoveryServiceImpl.java | 73 +++++- .../discovery/ApiDiscoveryTest.java | 10 +- .../response/QuotaResponseBuilderImpl.java | 2 +- .../QuotaResponseBuilderImplTest.java | 6 +- .../java/com/cloud/api/ApiResponseHelper.java | 75 ++++-- .../main/java/com/cloud/api/ApiServer.java | 4 +- .../java/com/cloud/user/AccountManager.java | 2 +- .../com/cloud/user/AccountManagerImpl.java | 248 +++++++++++++----- .../cloudstack/acl/ApiKeyPairManagerImpl.java | 37 +-- .../cloudstack/acl/RoleManagerImpl.java | 32 +-- .../cloud/user/AccountManagerImplTest.java | 78 ------ .../cloud/user/MockAccountManagerImpl.java | 34 +++ .../acl/ApiKeyPairManagerImplTest.java | 56 ---- .../cloudstack/acl/RoleManagerImplTest.java | 8 +- 32 files changed, 570 insertions(+), 348 deletions(-) create mode 100644 api/src/main/java/com/cloud/user/ApiKeyPairState.java delete mode 100644 server/src/test/java/org/apache/cloudstack/acl/ApiKeyPairManagerImplTest.java diff --git a/api/src/main/java/com/cloud/user/AccountService.java b/api/src/main/java/com/cloud/user/AccountService.java index c692235f66bb..fd1d278860d2 100644 --- a/api/src/main/java/com/cloud/user/AccountService.java +++ b/api/src/main/java/com/cloud/user/AccountService.java @@ -23,8 +23,12 @@ 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.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; @@ -120,6 +124,8 @@ User createUser(String userName, String password, String firstName, String lastN void checkAccess(Account account, AccessType accessType, boolean sameOwner, String apiName, ControlledEntity... entities) throws PermissionDeniedException; + void validateCallingUserHasAccessToDesiredUser(Long userId); + Long finalyzeAccountId(String accountName, Long domainId, Long projectId, boolean enabledOnly); /** @@ -133,6 +139,11 @@ User createUser(String userName, String password, String firstName, String lastN ListResponse getKeys(ListUserKeysCmd cmd); + List listKeyRules(ListUserKeyRulesCmd cmd); + + void deleteApiKey(DeleteUserKeysCmd cmd); + + void deleteApiKey(ApiKeyPair id); /** * Lists user two-factor authentication provider plugins * @return list of providers @@ -150,4 +161,7 @@ User createUser(String userName, String password, String firstName, String lastN 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/org/apache/cloudstack/acl/RoleService.java b/api/src/main/java/org/apache/cloudstack/acl/RoleService.java index 568080f1684d..f8321915089a 100644 --- a/api/src/main/java/org/apache/cloudstack/acl/RoleService.java +++ b/api/src/main/java/org/apache/cloudstack/acl/RoleService.java @@ -101,5 +101,24 @@ public interface RoleService { Permission getRolePermission(String permission); + List findAllRolePermissionsEntityBy(Long roleId); + 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 index 6a97b11e8000..ecce0ae50824 100644 --- a/api/src/main/java/org/apache/cloudstack/acl/apikeypair/ApiKeyPair.java +++ b/api/src/main/java/org/apache/cloudstack/acl/apikeypair/ApiKeyPair.java @@ -32,5 +32,7 @@ public interface ApiKeyPair extends ControlledEntity, InternalIdentity, Identity String getSecretKey(); String getName(); Date getRemoved(); - boolean validateDate(boolean throwException); + void setRemoved(Date date); + void validateDate(); + boolean hasEndDatePassed(); } 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 index 7e3da6d4e681..d2eb3bc6ce25 100644 --- a/api/src/main/java/org/apache/cloudstack/acl/apikeypair/ApiKeyPairService.java +++ b/api/src/main/java/org/apache/cloudstack/acl/apikeypair/ApiKeyPairService.java @@ -19,13 +19,11 @@ import java.util.List; public interface ApiKeyPairService { - List findAllPermissionsByKeyPairId(Long apiKeyPairId); + List findAllPermissionsByKeyPairId(Long apiKeyPairId, Long roleId); ApiKeyPair findByApiKey(String apiKey); ApiKeyPair findById(Long id); - void deleteApiKey(ApiKeyPair 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 ff264a2504f9..20c47f1a64f3 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"; @@ -35,6 +36,8 @@ public class ApiConstants { public static final String ALLOW_USER_FORCE_STOP_VM = "allowuserforcestopvm"; public static final String ANNOTATION = "annotation"; public static final String API_KEY = "apikey"; + public static final String API_KEY_FILTER = "apikeyfilter"; + public static final String HEADER_API_KEY = "apiKey"; public static final String ARCHIVED = "archived"; public static final String ARCH = "arch"; public static final String AS_NUMBER = "asnumber"; @@ -446,6 +449,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"; @@ -640,6 +644,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"; 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 index 7da6201aad71..51cda407644b 100644 --- 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 @@ -17,7 +17,6 @@ package org.apache.cloudstack.api.command.admin.user; import com.cloud.event.EventTypes; -import com.cloud.exception.InvalidParameterValueException; import com.cloud.user.Account; import org.apache.cloudstack.acl.apikeypair.ApiKeyPair; import org.apache.cloudstack.api.ACL; @@ -51,20 +50,13 @@ public long getEntityOwnerId() { return Account.ACCOUNT_ID_SYSTEM; } - private Long getId() { + public Long getId() { return id; } @Override public void execute() { - ApiKeyPair keyPair = apiKeyPairService.findById(getId()); - if (keyPair == null) { - throw new InvalidParameterValueException(String.format("No keypair found with the id [%s].", getId())); - } - apiKeyPairService.validateCallingUserHasAccessToDesiredUser(keyPair.getUserId()); - - apiKeyPairService.deleteApiKey(keyPair); - + _accountService.deleteApiKey(this); SuccessResponse response = new SuccessResponse(getCommandName()); this.setResponseObject(response); } 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 index 0d86f78b6b81..8c57adec2dcd 100644 --- 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 @@ -19,7 +19,6 @@ import com.cloud.exception.InsufficientCapacityException; -import com.cloud.exception.InvalidParameterValueException; import com.cloud.exception.ResourceUnavailableException; import com.cloud.user.Account; import java.util.List; @@ -28,7 +27,7 @@ import org.apache.cloudstack.api.ACL; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiConstants; -import org.apache.cloudstack.api.BaseCmd; +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; @@ -42,10 +41,10 @@ responseHasSensitiveInfo = false, since = "4.20.0") -public class ListUserKeyRulesCmd extends BaseCmd { +public class ListUserKeyRulesCmd extends BaseListDomainResourcesCmd { @ACL - @Parameter(name=ApiConstants.ID, type = CommandType.UUID, entityType = ApiKeyPairResponse.class, description = "ID of the keypair.", required = true) + @Parameter(name = ApiConstants.KEYPAIR_ID, type = CommandType.UUID, entityType = ApiKeyPairResponse.class, description = "ID of the keypair.", required = true) private Long id; public Long getId() { @@ -62,13 +61,7 @@ public long getEntityOwnerId() { @Override public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException { - ApiKeyPair keyPair = apiKeyPairService.findById(getId()); - if (keyPair == null) { - throw new InvalidParameterValueException(String.format("No keypair found with the id [%s].", getId())); - } - apiKeyPairService.validateCallingUserHasAccessToDesiredUser(keyPair.getUserId()); - - List permissions = apiKeyPairService.findAllPermissionsByKeyPairId(keyPair.getId()); + 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 index 19bf4e3cd380..ddcf8db51d78 100644 --- 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 @@ -45,13 +45,21 @@ public class ListUserKeysCmd extends BaseListDomainResourcesCmd { @ACL - @Parameter(name=ApiConstants.ID, type = CommandType.UUID, entityType = UserResponse.class, description = "ID of the user that owns the keys.") + @Parameter(name = ApiConstants.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.") + @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; + + protected Logger logger = LogManager.getLogger(getClass()); public Long getUserId() { @@ -62,6 +70,14 @@ 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()); 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 44715341aa95..6e52eef89730 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 @@ -58,10 +58,12 @@ public class RegisterUserKeysCmd extends BaseCmd { @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 for the API keypair.") + @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 for the API keypair.") + @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, " + 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 index 461be1fc86ea..7f8ba60a95e6 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/ApiKeyPairResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/ApiKeyPairResponse.java @@ -16,15 +16,19 @@ // under the License. package org.apache.cloudstack.api.response; -import com.cloud.serializer.Param; +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; -import java.util.Date; - @EntityReference(value = ApiKeyPair.class) public class ApiKeyPairResponse extends BaseResponseWithAnnotations { @SerializedName(ApiConstants.NAME) @@ -44,12 +48,12 @@ public class ApiKeyPairResponse extends BaseResponseWithAnnotations { private String userId; @SerializedName(ApiConstants.USERNAME) - @Param(description = "User name of the keypair's owner.") - private String userName; + @Param(description = "Username of the keypair's owner.") + private String username; - @SerializedName(ApiConstants.UUID) - @Param(description = "UUID of the API keypair.", isSensitive = true) - private String uuid; + @SerializedName(ApiConstants.ID) + @Param(description = "ID of the API keypair.", isSensitive = true) + private String id; @SerializedName(ApiConstants.DESCRIPTION) @Param(description = "Keypair description.") @@ -68,12 +72,20 @@ public class ApiKeyPairResponse extends BaseResponseWithAnnotations { private Date created; @SerializedName(ApiConstants.ACCOUNT_TYPE) - @Param(description = "Account type (admin, domain-admin, user).") - private Integer accountType; + @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 Long roleId; + private String roleId; @SerializedName(ApiConstants.ROLE_TYPE) @Param(description = "Type of the role (Admin, ResourceAdmin, DomainAdmin, User).") @@ -83,6 +95,10 @@ public class ApiKeyPairResponse extends BaseResponseWithAnnotations { @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; @@ -97,7 +113,7 @@ public class ApiKeyPairResponse extends BaseResponseWithAnnotations { @SerializedName(ApiConstants.STATE) @Param(description = "State of the keypair.") - private String state; + private ApiKeyPairState state; public String getApiKey() { return userApiKey; @@ -115,12 +131,12 @@ public void setSecretKey(String secretKey) { this.userSecretKey = secretKey; } - public String getUuid() { - return this.uuid; + public String getId() { + return this.id; } - public void setUuid(String uuid) { - this.uuid = uuid; + public void setId(String id) { + this.id = id; } public String getUserId() { @@ -171,28 +187,36 @@ public void setName(String name) { this.name = name; } - public Integer getAccountType() { + public String getAccountType() { return accountType; } - public void setAccountType(Integer accountType) { + public void setAccountType(String accountType) { this.accountType = accountType; } - public Long getRoleId() { + public String getRoleId() { return roleId; } - public void setRoleId(Long roleId) { + public void setRoleId(String roleId) { this.roleId = roleId; } - public String getUserName() { - return userName; + public String getAccountId() { + return accountId; } - public void setUserName(String userName) { - this.userName = userName; + public void setAccountId(String accountId) { + this.accountId = accountId; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; } public String getRoleType() { @@ -235,11 +259,27 @@ public void setDomainPath(String domainPath) { this.domainPath = domainPath; } - public String getState() { + public ApiKeyPairState getState() { return state; } - public void setState(String 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/engine/schema/src/main/java/com/cloud/user/dao/AccountDaoImpl.java b/engine/schema/src/main/java/com/cloud/user/dao/AccountDaoImpl.java index e00089182ac6..761b22867259 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 @@ -165,6 +165,7 @@ public Ternary findUserAccountByApiKey(String apiKey) keyPair.setEndDate(rs.getTimestamp(13)); keyPair.setSecretKey(DBEncryptionUtil.decrypt(rs.getString(14))); keyPair.setRemoved(rs.getTimestamp(15)); + keyPair.setApiKey(apiKey); userAcctTernary = new Ternary(user, account, keyPair); } 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 bd8a6d471db3..caf5278e1d92 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 @@ -50,4 +50,8 @@ public void setSortOrder(long sortOrder) { public long getSortOrder() { return sortOrder; } + + public void setApiKeyPairId(long id) { + this.apiKeyPairId = id; + } } 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 index 6b1ba118ec84..8419784092d1 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/acl/ApiKeyPairVO.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/acl/ApiKeyPairVO.java @@ -119,23 +119,22 @@ public ApiKeyPairVO(Long id, Long userId) { this.userId = userId; } - public boolean validateDate(boolean throwException) throws PermissionDeniedException { + 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) { - if (throwException) { - throw new PermissionDeniedException(String.format("Keypair is not valid yet, start date: %s", keypairStart)); - } - return false; + throw new PermissionDeniedException(String.format("Keypair is not valid yet, start date: %s", keypairStart)); } if (keypairExpiration != null && now.compareTo(keypairExpiration) >= 0) { - if (throwException) { - throw new PermissionDeniedException(String.format("Keypair is expired, expiration date: %s", keypairExpiration)); - } - return false; + throw new PermissionDeniedException(String.format("Keypair is expired, expiration date: %s", keypairExpiration)); } - return true; + } + + public boolean hasEndDatePassed() { + Date now = DateTime.now().toDate(); + Date keypairExpiration = this.getEndDate(); + return keypairExpiration != null && now.compareTo(keypairExpiration) >= 0; } public long getId() { diff --git a/plugins/api/discovery/pom.xml b/plugins/api/discovery/pom.xml index 6426dcd70a51..94b0e85d8870 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 + 4.20.0.0-SNAPSHOT + 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..bf335e032daf 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 listApisCmd); } 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..2719e3c92f1d 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,13 +26,17 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; import javax.inject.Inject; import org.apache.cloudstack.acl.APIChecker; import org.apache.cloudstack.acl.Role; +import org.apache.cloudstack.acl.RolePermissionEntity; import org.apache.cloudstack.acl.RoleService; import org.apache.cloudstack.acl.RoleType; +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,8 @@ 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,13 +301,14 @@ 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); return response; } + private static ApiDiscoveryResponse getApiDiscoveryResponseWithAccessibleParams(String name, Account account) { if (Account.Type.ADMIN.equals(account.getType())) { return s_apiNameDiscoveryResponseMap.get(name); @@ -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/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 1c486759e43f..0d889c1df72a 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 @@ -235,7 +235,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 fd3595258937..a02ee68b63cc 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 @@ -609,7 +609,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)); } @@ -625,7 +625,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)); } @@ -641,7 +641,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/server/src/main/java/com/cloud/api/ApiResponseHelper.java b/server/src/main/java/com/cloud/api/ApiResponseHelper.java index 4bd1a6ac0bb3..b93435d2a38a 100644 --- a/server/src/main/java/com/cloud/api/ApiResponseHelper.java +++ b/server/src/main/java/com/cloud/api/ApiResponseHelper.java @@ -47,12 +47,18 @@ import com.cloud.dc.dao.ASNumberDao; import com.cloud.dc.dao.ASNumberRangeDao; import com.cloud.dc.dao.VlanDetailsDao; +import com.cloud.domain.dao.DomainDao; import com.cloud.hypervisor.Hypervisor; import com.cloud.storage.BucketVO; +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; @@ -505,6 +511,12 @@ public class ApiResponseHelper implements ResponseGenerator { @Inject NetworkServiceMapDao ntwkSrvcDao; @Inject + private RoleDao roleDao; + @Inject + private AccountDao accountDao; + @Inject + private DomainDao domainDao; + @Inject FirewallRulesDao firewallRulesDao; @Inject UserDataDao userDataDao; @@ -5384,41 +5396,66 @@ public BucketResponse createBucketResponse(Bucket bucket) { 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 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()); + } + + protected void populateUserInApiKeyPairResponse(ApiKeyPair keyPair, ApiKeyPairResponse apiKeyPairResponse) { User user = ApiDBUtils.findUserById(keyPair.getUserId()); + apiKeyPairResponse.setUserId(user.getUuid()); + apiKeyPairResponse.setUsername(user.getUsername()); + } + + 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.setUuid(keyPair.getUuid()); + apiKeyPairResponse.setId(keyPair.getUuid()); apiKeyPairResponse.setCreated(keyPair.getCreated()); apiKeyPairResponse.setStartDate(keyPair.getStartDate()); apiKeyPairResponse.setEndDate(keyPair.getEndDate()); - apiKeyPairResponse.setUserId(keyPair.getUserId().toString()); - AccountJoinVO account = ApiDBUtils.findAccountViewById(user.getAccountId()); - apiKeyPairResponse.setUserId(String.valueOf(user.getId())); - apiKeyPairResponse.setUserName(user.getFirstname()); - apiKeyPairResponse.setAccountType(account.getType().ordinal()); - apiKeyPairResponse.setDomainId(account.getDomainUuid()); - apiKeyPairResponse.setDomainName(account.getDomainName()); - StringBuilder domainPath = new StringBuilder("ROOT"); - (domainPath.append(account.getDomainPath())).deleteCharAt(domainPath.length() - 1); - apiKeyPairResponse.setDomainPath(domainPath.toString()); - Account.State state = Account.State.ENABLED; + ApiKeyPairState state = ApiKeyPairState.ENABLED; if (keyPair.getRemoved() != null) { - state = Account.State.REMOVED; - } else if (!keyPair.validateDate(false)) { - state = Account.State.DISABLED; + state = ApiKeyPairState.REMOVED; + } else if (keyPair.hasEndDatePassed()) { + state = ApiKeyPairState.EXPIRED; } - apiKeyPairResponse.setState(state.toString()); + apiKeyPairResponse.setState(state); - // set async job + User user = ApiDBUtils.findUserById(keyPair.getUserId()); + AccountJoinVO account = ApiDBUtils.findAccountViewById(user.getAccountId()); if (account.getJobId() != null) { apiKeyPairResponse.setJobId(account.getJobUuid()); apiKeyPairResponse.setJobStatus(account.getJobStatus()); } - - return apiKeyPairResponse; } @Override diff --git a/server/src/main/java/com/cloud/api/ApiServer.java b/server/src/main/java/com/cloud/api/ApiServer.java index 38524f742071..88808d20961f 100644 --- a/server/src/main/java/com/cloud/api/ApiServer.java +++ b/server/src/main/java/com/cloud/api/ApiServer.java @@ -1007,7 +1007,7 @@ public boolean verifyRequest(final Map requestParameters, fina return false; } - keyPair.validateDate(true); + keyPair.validateDate(); secretKey = keyPair.getSecretKey(); if (secretKey == null) { @@ -1033,7 +1033,7 @@ public boolean verifyRequest(final Map requestParameters, fina } CallContext.register(user, account); - List keyPairPermissions = keyPairManager.findAllPermissionsByKeyPairId(keyPair.getId()); + List keyPairPermissions = keyPairManager.findAllPermissionsByKeyPairId(keyPair.getId(), account.getRoleId()); if (commandAvailable(remoteAddress, commandName, user, keyPairPermissions.toArray(new ApiKeyPairPermission[0]))) { return true; diff --git a/server/src/main/java/com/cloud/user/AccountManager.java b/server/src/main/java/com/cloud/user/AccountManager.java index 5d8c81c2719c..e002bbe95f3b 100644 --- a/server/src/main/java/com/cloud/user/AccountManager.java +++ b/server/src/main/java/com/cloud/user/AccountManager.java @@ -202,5 +202,5 @@ void buildACLViewSearchCriteria(SearchCriteria s void validateUserPasswordAndUpdateIfNeeded(String newPassword, UserVO user, String currentPassword, boolean skipCurrentPassValidation); - void checkApiAccess(Account caller, String command); + void checkApiAccess(Account caller, String command); } diff --git a/server/src/main/java/com/cloud/user/AccountManagerImpl.java b/server/src/main/java/com/cloud/user/AccountManagerImpl.java index 6317affc6a63..d0eb44bcc04e 100644 --- a/server/src/main/java/com/cloud/user/AccountManagerImpl.java +++ b/server/src/main/java/com/cloud/user/AccountManagerImpl.java @@ -28,6 +28,7 @@ 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; @@ -65,10 +66,14 @@ 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.BaseCmd; 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.RegisterUserKeysCmd; @@ -887,7 +892,7 @@ protected boolean cleanupAccount(AccountVO account, long callerUserId, Account c for (UserVO user : users) { // remove apikeys List apiKeyPairs = apiKeyPairDao.listApiKeysByUserOrApiKeyId(user.getId(), null).first(); - apiKeyPairs.stream().forEach(keyPair -> apiKeyPairService.deleteApiKey(keyPair)); + apiKeyPairs.stream().forEach(keyPair -> _accountService.deleteApiKey(keyPair)); if (!_userDao.remove(user.getId())) { logger.error("Unable to delete user: " + user + " as a part of account " + account + " cleanup"); @@ -2176,7 +2181,7 @@ public boolean deleteUser(DeleteUserCmd deleteUserCmd) { checkAccountAndAccess(user, account); // remove apikeys List apiKeyPairs = apiKeyPairDao.listApiKeysByUserOrApiKeyId(id, null).first(); - apiKeyPairs.stream().forEach(keyPair -> apiKeyPairService.deleteApiKey(keyPair)); + apiKeyPairs.stream().forEach(keyPair -> deleteApiKey(keyPair)); return _userDao.remove(id); } @@ -2860,7 +2865,7 @@ public ListResponse getKeys(ListUserKeysCmd cmd) { ListResponse finalResponse = new ListResponse<>(); List responses = new ArrayList<>(); - if (!cmd.listAll() || cmd.getKeyId() != null) { + if (cmd.getApiKeyFilter() != null || cmd.getKeyId() != null) { fetchOnlyOneKeypair(responses, cmd); finalResponse.setResponses(responses); return finalResponse; @@ -2878,15 +2883,16 @@ private void fetchOnlyOneKeypair(List responses, ListUserKey ApiKeyPair keyPair; if (cmd.getKeyId() != null) { keyPair = _accountService.getKeyPairById(cmd.getKeyId()); - } else if (cmd.getUserId() != null) { - keyPair = _accountService.getLatestUserKeyPair(cmd.getUserId()); } else { - keyPair = _accountService.getLatestUserKeyPair(CallContext.current().getCallingUser().getId()); - } - if (keyPair == null) { - throw new InvalidParameterValueException("No api key was found with the parameters specified"); + keyPair = _accountService.getKeyPairByApiKey(cmd.getApiKeyFilter()); } + + validateKeyPairIsNotNull(keyPair); + validateAccessingKeyPairPermissionsIsSupersetOfAccessedKeyPair(keyPair, cmd); + apiKeyPairService.validateCallingUserHasAccessToDesiredUser(keyPair.getUserId()); + markExpiredKeysWithStateExpired(keyPair); + addKeypairResponse(keyPair, responses, cmd); } @@ -2897,16 +2903,157 @@ private Integer fetchMultipleKeypairs(List responses, ListUs users = List.of(cmd.getUserId()); } else { User callerUser = CallContext.current().getCallingUser(); - users = isAdmin(callerUser.getAccountId()) ? queryService.searchForAccessableUsers() : List.of(callerUser.getId()); + users = cmd.listAll() && isAdmin(callerUser.getAccountId()) ? queryService.searchForAccessableUsers() : List.of(callerUser.getId()); } Pair, Integer> keyPairs = apiKeyPairDao.listByUserIdsPaginated(users, cmd); - for (ApiKeyPairVO keyPair : keyPairs.first()) { - addKeypairResponse(keyPair, responses, 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(String.format("Could not complete request.")); + } + } + + @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())); + } + } + + 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()))); + } + + private Boolean isApiKeySupersetOfPermission(List baseKeyPairPermissions, List comparedPermissions) { + Map apiNameToBaseKeyPermissions = roleService.getRoleRulesAndPermissions(baseKeyPairPermissions); + + return roleService.roleHasPermission(apiNameToBaseKeyPermissions, comparedPermissions); + } + + private Boolean validatePermission(List supersetPermissions, RolePermissionEntity comparedPermission) { + for (RolePermissionEntity supersetPermission : supersetPermissions) { + if (!supersetPermission.getRule().matches(comparedPermission.getRule().getRuleString())) { + continue; + } + + if (!supersetPermission.getPermission().equals(RolePermissionEntity.Permission.ALLOW) && (comparedPermission.getPermission() == RolePermissionEntity.Permission.ALLOW)) { + return false; + } + return true; + } + return false; + } + + private void markExpiredKeysWithStateExpired(ApiKeyPair apiKeyPair) { + if (apiKeyPair.hasEndDatePassed()) { + internalDeleteApiKey(apiKeyPair); + } + } + + @Override + 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 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()); + } + + @Override + public String getAccessingApiKey(BaseCmd cmd) { + if (cmd == null) { + return null; + } + 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("apiKey") + 9; + return jobString.substring(indexOfApiKey, jobString.indexOf("\"", indexOfApiKey)); + } + private void addKeypairResponse(ApiKeyPair keyPair, List responses, ListUserKeysCmd cmd) { if (keyPair == null) { return; @@ -2921,6 +3068,11 @@ public ApiKeyPair getKeyPairById(Long id) { return apiKeyPairDao.findById(id); } + @Override + public ApiKeyPair getKeyPairByApiKey(String apiKey) { + return apiKeyPairDao.findByApiKey(apiKey); + } + 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()); @@ -3099,22 +3251,35 @@ private String createUserSecretKey(long userId, ApiKeyPairVO newApiKeyPair) { */ @DB private ApiKeyPairVO validateAndPersistKeyPairAndPermissions(Account account, ApiKeyPairVO newApiKeyPair, List> rules, Boolean accessedByApiKey, String apiKey) { - final ApiKeyPairVO savedApiKeyPair = apiKeyPairDao.persist(newApiKeyPair); - final Role accountRole = roleService.findRole(account.getRoleId()); List allPermissions = (accessedByApiKey == null || BooleanUtils.isFalse(accessedByApiKey)) ? getAllAccountRolePermissions(accountRole) : getAllKeypairPermissions(apiKey); - List validatedPermissions; + List permissions; if (CollectionUtils.isEmpty(rules)) { - validatedPermissions = allPermissions.stream().map(permission -> new ApiKeyPairPermissionVO(savedApiKeyPair.getId(), + permissions = allPermissions.stream().map(permission -> new ApiKeyPairPermissionVO(0, permission.getRule().toString(), permission.getPermission(), permission.getDescription())).collect(Collectors.toList()); } else { - validatedPermissions = validatePermissions(rules, savedApiKeyPair, allPermissions); - } + 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); + }); - validatedPermissions.forEach(permission -> apiKeyPairPermissionsDao.persist(permission)); return savedApiKeyPair; } @@ -3137,51 +3302,12 @@ private List getAllKeypairPermissions(String apiKey) { throw new InvalidParameterValueException("API key not present in URL, cannot fetch API key rules"); } ApiKeyPair apiKeyPair = keyPairManager.findByApiKey(apiKey); - List allApiKeyRolePermissions = keyPairManager.findAllPermissionsByKeyPairId(apiKeyPair.getId()); + Account account = _accountDao.findById(apiKeyPair.getAccountId()); + List allApiKeyRolePermissions = keyPairManager.findAllPermissionsByKeyPairId(apiKeyPair.getId(), account.getRoleId()); return allApiKeyRolePermissions.stream().map(permission -> (RolePermissionEntity) permission) .collect(Collectors.toList()); } - /*** - * Validates if the user permissions are a superset of permissions of the Keypair that is being created - * @param rules are the rules passed to the API which are being validated - * @param savedApiKeyPair is the new keypair being created - * @throws InvalidParameterValueException if the user's permissions are not a superset of the Keypair. - */ - private List validatePermissions(List> rules, ApiKeyPairVO savedApiKeyPair, - List allAccountPermissions) { - - List validatedPermissions = new ArrayList<>(); - for (Map ruleDetail : rules) { - String apiName = ruleDetail.get(ApiConstants.RULE).toString(); - RolePermission.Permission rulePermission = (RolePermission.Permission) ruleDetail.get(ApiConstants.PERMISSION); - String ruleDescription = (String) ruleDetail.get(ApiConstants.DESCRIPTION); - - matchARuleWithAccountRules(allAccountPermissions, apiName, rulePermission, savedApiKeyPair, ruleDescription, validatedPermissions); - } - return validatedPermissions; - } - - private void matchARuleWithAccountRules(List allAccountPermissions, String apiName, RolePermission.Permission rulePermission, - ApiKeyPairVO savedApiKeyPair, String ruleDescription, List validatedPermissions) { - for (RolePermissionEntity accountPermission : allAccountPermissions) { - if (!accountPermission.getRule().matches(apiName)) { - continue; - } - - if (!accountPermission.getPermission().equals(RolePermissionEntity.Permission.ALLOW) && rulePermission.equals(RolePermissionEntity.Permission.ALLOW)) { - apiKeyPairDao.remove(savedApiKeyPair.getId()); - throw new InvalidParameterValueException(String.format("API %s is not allowed based on the user's account, a valid " + - "keypair rule must be also allowed at the account level", apiName)); - } - validatedPermissions.add(new ApiKeyPairPermissionVO(savedApiKeyPair.getId(), apiName, rulePermission, ruleDescription)); - return; - } - apiKeyPairDao.remove(savedApiKeyPair.getId()); - throw new InvalidParameterValueException(String.format("API %s was not found on the user's account, a valid " + - "keypair rule must be also present at the account level", apiName)); - } - @Override public void buildACLSearchBuilder(SearchBuilder sb, Long domainId, boolean isRecursive, List permittedAccounts, ListProjectResourcesCriteria listProjectResourcesCriteria) { 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 dd1d5bdd0a8a..21b51a7cded8 100644 --- a/server/src/main/java/org/apache/cloudstack/acl/ApiKeyPairManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/acl/ApiKeyPairManagerImpl.java @@ -17,7 +17,6 @@ package org.apache.cloudstack.acl; import com.cloud.exception.InvalidParameterValueException; -import com.cloud.exception.PermissionDeniedException; import com.cloud.user.AccountManager; import com.cloud.user.User; import com.cloud.user.dao.UserDao; @@ -27,11 +26,11 @@ 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.acl.dao.RolePermissionsDao; import org.apache.cloudstack.query.QueryService; -import org.apache.cloudstack.utils.baremetal.BaremetalUtils; +import org.apache.commons.collections.CollectionUtils; import javax.inject.Inject; -import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; @@ -49,14 +48,22 @@ public class ApiKeyPairManagerImpl extends ManagerBase implements ApiKeyPairServ private QueryService queryService; @Inject private AccountManager accountManager; + @Inject + private RolePermissionsDao rolePermissionsDao; @Override - public List findAllPermissionsByKeyPairId(Long apiKeyPairId) { + public List findAllPermissionsByKeyPairId(Long apiKeyPairId, Long roleId) { List allPermissions = apiKeyPairPermissionsDao.findAllByKeyPairIdSorted(apiKeyPairId); - if (allPermissions != null) { + if (CollectionUtils.isNotEmpty(allPermissions)) { return allPermissions.stream().map(p -> (ApiKeyPairPermission) p).collect(Collectors.toList()); } - return Collections.emptyList(); + 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 @@ -69,24 +76,6 @@ public ApiKeyPair findById(Long id) { return apiKeyPairDao.findById(id); } - @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) { - throw new PermissionDeniedException("User id : " + user.getUuid() + " is system account, deletion of API Keys is not allowed."); - } - - List permissions = apiKeyPairPermissionsDao.findAllByApiKeyPairId(keyPair.getId()); - for (ApiKeyPairPermission permission : permissions) { - apiKeyPairPermissionsDao.remove(permission.getId()); - } - apiKeyPairDao.remove(keyPair.getId()); - } - @Override public void validateCallingUserHasAccessToDesiredUser(Long userId) { List accessableUsers = queryService.searchForAccessableUsers(); 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 c85038a0763d..a6e3f9e6a19a 100644 --- a/server/src/main/java/org/apache/cloudstack/acl/RoleManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/acl/RoleManagerImpl.java @@ -456,14 +456,14 @@ public int removeRolesIfNeeded(List roles) { } 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; } @@ -474,17 +474,19 @@ public 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 List findAllRolePermissionsEntityBy(final Long roleId) { + List permissions = rolePermissionsDao.findAllByRoleIdSorted(roleId); + if (permissions != null) { + return new ArrayList<>(permissions); + } + return Collections.emptyList(); + } + + @Override + public boolean roleHasPermission(Map sourceRolePermissions, 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()) { @@ -510,12 +512,12 @@ protected boolean roleHasPermission(Map sourceRolePermission /** * 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. + * @param rolePermissions permissions from Role or Api Key. */ - public Map getRoleRulesAndPermissions(Long roleId) { + 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()) { diff --git a/server/src/test/java/com/cloud/user/AccountManagerImplTest.java b/server/src/test/java/com/cloud/user/AccountManagerImplTest.java index 9be039466e6f..ec595efdeb94 100644 --- a/server/src/test/java/com/cloud/user/AccountManagerImplTest.java +++ b/server/src/test/java/com/cloud/user/AccountManagerImplTest.java @@ -1324,42 +1324,6 @@ public void createApiKeyAndSecretKeyTestAllowedOnAccount() { Mockito.verify(keyPairPermissionsDaoMock, Mockito.times(1)).persist(Mockito.any(ApiKeyPairPermissionVO.class)); } - @Test - public void createApiKeyAndSecretKeyTestWithDeniedThatIsAllowedOnAccount() { - 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(registerUserKeysCmdMock.getRules()).thenReturn(rules); - Mockito.when(registerUserKeysCmdMock.getUserId()).thenReturn(userId); - - 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(keyPairPermissionsDaoMock.persist(Mockito.any(ApiKeyPairPermissionVO.class))).thenReturn(permissionVO); - Mockito.when(keyPairDaoMock.findBySecretKey(Mockito.anyString())).thenReturn(null); - Mockito.when(roleServiceMock.findAllPermissionsBy(Mockito.anyLong())).thenReturn(List.of( - new RolePermissionVO(1L, "api", RolePermissionEntity.Permission.ALLOW, "description") - )); - Mockito.when(roleServiceMock.findRole(Mockito.anyLong())).thenReturn(new RoleVO()); - - ApiKeyPair keyPairVO = accountManagerImpl.createApiKeyAndSecretKey(registerUserKeysCmdMock); - - Assert.assertEquals((long) keyPairVO.getUserId(), userId); - } - @Test public void createApiAndSecretKeyTestWithNonEmptyDates() { CallContext.register(callingUser, callingAccount); @@ -1480,48 +1444,6 @@ public void createApiAndSecretKeyTestWithInvalidDate() { Assert.assertEquals(response.getDescription(), "key description"); } - @Test - public void createApiAndSecretKeyTestWithMultipleAllowedPermissions() { - 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" - )); - Mockito.when(registerUserKeysCmdMock.getRules()).thenReturn(rules); - Mockito.when(registerUserKeysCmdMock.getUserId()).thenReturn(userId); - - 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(keyPairPermissionsDaoMock.persist(Mockito.any(ApiKeyPairPermissionVO.class))).thenReturn(permissionVO); - Mockito.when(keyPairDaoMock.findBySecretKey(Mockito.anyString())).thenReturn(null); - Mockito.when(roleServiceMock.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(roleServiceMock.findRole(Mockito.anyLong())).thenReturn(new RoleVO()); - - accountManagerImpl.createApiKeyAndSecretKey(registerUserKeysCmdMock); - Mockito.verify(keyPairDaoMock, Mockito.times(0)).remove(Mockito.anyLong()); - Mockito.verify(keyPairPermissionsDaoMock, Mockito.times(2)).persist(Mockito.any(ApiKeyPairPermissionVO.class)); - } - @Test(expected = InvalidParameterValueException.class) public void createApiAndSecretKeyTestWithMultipleAllowedPermissionsOneDenied() { CallContext.register(callingUser, callingAccount); diff --git a/server/src/test/java/com/cloud/user/MockAccountManagerImpl.java b/server/src/test/java/com/cloud/user/MockAccountManagerImpl.java index 5be1cf712359..5ef12bb95a08 100644 --- a/server/src/test/java/com/cloud/user/MockAccountManagerImpl.java +++ b/server/src/test/java/com/cloud/user/MockAccountManagerImpl.java @@ -37,10 +37,14 @@ 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.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.RegisterUserKeysCmd; @@ -441,6 +445,11 @@ public void checkAccess(Account account, AccessType accessType, boolean sameOwne // 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 @@ -457,6 +466,21 @@ public ListResponse getKeys(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; @@ -477,6 +501,16 @@ public ApiKeyPair getKeyPairById(Long id) { return null; } + @Override + public ApiKeyPair getKeyPairByApiKey(String apiKey) { + return null; + } + + @Override + public String getAccessingApiKey(BaseCmd cmd) { + return null; + } + public void checkApiAccess(Account account, String command) throws PermissionDeniedException { } diff --git a/server/src/test/java/org/apache/cloudstack/acl/ApiKeyPairManagerImplTest.java b/server/src/test/java/org/apache/cloudstack/acl/ApiKeyPairManagerImplTest.java deleted file mode 100644 index 6d27d1f73d34..000000000000 --- a/server/src/test/java/org/apache/cloudstack/acl/ApiKeyPairManagerImplTest.java +++ /dev/null @@ -1,56 +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.acl; - -import com.cloud.user.UserVO; -import com.cloud.user.dao.UserDao; -import org.apache.cloudstack.acl.dao.ApiKeyPairDao; -import org.apache.cloudstack.acl.dao.ApiKeyPairPermissionsDao; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.junit.MockitoJUnitRunner; - -import java.util.List; - -@RunWith(MockitoJUnitRunner.class) -public class ApiKeyPairManagerImplTest { - @Mock - ApiKeyPairPermissionsDao apiKeyPairPermissionsDao; - @Mock - ApiKeyPairDao apiKeyPairDao; - @Mock - UserDao userDao; - @InjectMocks - ApiKeyPairManagerImpl apiKeyPairManager; - - @Before - public void setup() { - Mockito.when(apiKeyPairPermissionsDao.findAllByApiKeyPairId(Mockito.any())).thenReturn(List.of(new ApiKeyPairPermissionVO())); - Mockito.when(userDao.findByIdIncludingRemoved(Mockito.any())).thenReturn(new UserVO()); - } - - @Test - public void deleteApiKeyTestOnePermission() { - apiKeyPairManager.deleteApiKey(new ApiKeyPairVO(1L, 1L)); - Mockito.verify(apiKeyPairPermissionsDao, Mockito.times(1)).remove(Mockito.anyLong()); - Mockito.verify(apiKeyPairDao, Mockito.times(1)).remove(Mockito.anyLong()); - } -} 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..b6e2e0d2138a 100644 --- a/server/src/test/java/org/apache/cloudstack/acl/RoleManagerImplTest.java +++ b/server/src/test/java/org/apache/cloudstack/acl/RoleManagerImplTest.java @@ -279,7 +279,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 +290,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, Collections.singletonList(rolePermission1Mock)); Assert.assertFalse(result); } @@ -301,7 +301,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, Collections.singletonList(rolePermission1Mock)); Assert.assertFalse(result); } @@ -310,7 +310,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); From 61e09400fde78b57567c865f0b9df17a64d7def6 Mon Sep 17 00:00:00 2001 From: "klaus.freitas.scclouds" Date: Thu, 17 Oct 2024 17:55:50 -0300 Subject: [PATCH 20/22] fix db migration logic, list permissions with keys --- .../command/admin/user/DeleteUserKeysCmd.java | 2 +- .../upgrade/dao/Upgrade41910to42000.java | 2 +- .../com/cloud/user/AccountManagerImpl.java | 27 +++++++++---------- 3 files changed, 15 insertions(+), 16 deletions(-) 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 index 51cda407644b..bc758ce8fe36 100644 --- 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 @@ -33,7 +33,7 @@ public class DeleteUserKeysCmd extends BaseAsyncCmd { @ACL - @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = ApiKeyPairResponse.class, required = true, description = "ID of the keypair to be deleted.") + @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 diff --git a/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade41910to42000.java b/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade41910to42000.java index b2e39cf34a16..040657054cef 100644 --- a/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade41910to42000.java +++ b/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade41910to42000.java @@ -64,7 +64,7 @@ public InputStream[] getPrepareScripts() { 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 " + + PreparedStatement pstmt = conn.prepareStatement("SELECT u.id, u.api_key, u.secret_key, a.domain_id, u.account_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(); diff --git a/server/src/main/java/com/cloud/user/AccountManagerImpl.java b/server/src/main/java/com/cloud/user/AccountManagerImpl.java index 4884a58ac332..77f7e86cfc4a 100644 --- a/server/src/main/java/com/cloud/user/AccountManagerImpl.java +++ b/server/src/main/java/com/cloud/user/AccountManagerImpl.java @@ -80,6 +80,7 @@ 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; @@ -2987,20 +2988,6 @@ private Boolean isApiKeySupersetOfPermission(List baseKeyP return roleService.roleHasPermission(apiNameToBaseKeyPermissions, comparedPermissions); } - private Boolean validatePermission(List supersetPermissions, RolePermissionEntity comparedPermission) { - for (RolePermissionEntity supersetPermission : supersetPermissions) { - if (!supersetPermission.getRule().matches(comparedPermission.getRule().getRuleString())) { - continue; - } - - if (!supersetPermission.getPermission().equals(RolePermissionEntity.Permission.ALLOW) && (comparedPermission.getPermission() == RolePermissionEntity.Permission.ALLOW)) { - return false; - } - return true; - } - return false; - } - private void markExpiredKeysWithStateExpired(ApiKeyPair apiKeyPair) { if (apiKeyPair.hasEndDatePassed()) { internalDeleteApiKey(apiKeyPair); @@ -3073,6 +3060,18 @@ private void addKeypairResponse(ApiKeyPair keyPair, List res 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); } From b9503534a40fa31b0e2fcae45ceef100b6651e9d Mon Sep 17 00:00:00 2001 From: "klaus.freitas.scclouds" Date: Tue, 22 Oct 2024 13:46:34 -0300 Subject: [PATCH 21/22] fixing tests --- .../cloud/user/AccountManagerImplTest.java | 6 +++-- .../cloudstack/acl/RoleManagerImplTest.java | 24 +++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/server/src/test/java/com/cloud/user/AccountManagerImplTest.java b/server/src/test/java/com/cloud/user/AccountManagerImplTest.java index ec595efdeb94..d2bf1ef12690 100644 --- a/server/src/test/java/com/cloud/user/AccountManagerImplTest.java +++ b/server/src/test/java/com/cloud/user/AccountManagerImplTest.java @@ -1303,8 +1303,6 @@ public void createApiKeyAndSecretKeyTestAllowedOnAccount() { Mockito.when(registerUserKeysCmdMock.getRules()).thenReturn(rules); Mockito.when(registerUserKeysCmdMock.getUserId()).thenReturn(userId); - - UserVO mockUser = new UserVO(userId); ApiKeyPairPermissionVO permissionVO = new ApiKeyPairPermissionVO(); ApiKeyPairVO apiKeyPairVO = new ApiKeyPairVO(); apiKeyPairVO.setUserId(userId); @@ -1319,6 +1317,8 @@ public void createApiKeyAndSecretKeyTestAllowedOnAccount() { new RolePermissionVO(1L, "api", RolePermissionEntity.Permission.ALLOW, "description") )); Mockito.when(roleServiceMock.findRole(Mockito.anyLong())).thenReturn(new RoleVO()); + Mockito.doReturn(true).when(roleServiceMock).roleHasPermission(Mockito.any(), Mockito.any()); + Assert.assertEquals((long) accountManagerImpl.createApiKeyAndSecretKey(registerUserKeysCmdMock).getUserId(), userId); Mockito.verify(keyPairPermissionsDaoMock, Mockito.times(1)).persist(Mockito.any(ApiKeyPairPermissionVO.class)); @@ -1359,6 +1359,8 @@ public void createApiAndSecretKeyTestWithNonEmptyDates() { new RolePermissionVO(1L, "api", RolePermissionEntity.Permission.ALLOW, "description") )); Mockito.when(roleServiceMock.findRole(Mockito.anyLong())).thenReturn(new RoleVO()); + Mockito.doReturn(true).when(roleServiceMock).roleHasPermission(Mockito.any(), Mockito.any()); + ApiKeyPair response = accountManagerImpl.createApiKeyAndSecretKey(registerUserKeysCmdMock); Mockito.verify(keyPairDaoMock, Mockito.times(1)).persist(Mockito.any(ApiKeyPairVO.class)); 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 b6e2e0d2138a..49070a3a9e6a 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,17 @@ public void setUpRoleVisibilityTests() { Mockito.doReturn(RolePermissionEntity.Permission.ALLOW).when(rolePermission1Mock).getPermission(); Mockito.doReturn(RolePermissionEntity.Permission.ALLOW).when(rolePermission2Mock).getPermission(); - List lessPermissionsRolePermissions = Collections.singletonList(rolePermission1Mock); + List lessPermissionsRolePermissions = Collections.singletonList(rolePermission1Mock); Mockito.doReturn(1L).when(lessPermissionsRoleMock).getId(); - Mockito.when(roleManagerImpl.findAllPermissionsBy(1L)).thenReturn(lessPermissionsRolePermissions); + Mockito.when(roleManagerImpl.findAllRolePermissionsEntityBy(lessPermissionsRoleMock.getId())).thenReturn(lessPermissionsRolePermissions); - List morePermissionsRolePermissions = List.of(rolePermission1Mock, rolePermission2Mock); + List morePermissionsRolePermissions = List.of(rolePermission1Mock, rolePermission2Mock); Mockito.doReturn(2L).when(morePermissionsRoleMock).getId(); - Mockito.when(roleManagerImpl.findAllPermissionsBy(morePermissionsRoleMock.getId())).thenReturn(morePermissionsRolePermissions); + Mockito.when(roleManagerImpl.findAllRolePermissionsEntityBy(morePermissionsRoleMock.getId())).thenReturn(morePermissionsRolePermissions); - List differentPermissionsRolePermissions = Collections.singletonList(rolePermission2Mock); + List differentPermissionsRolePermissions = Collections.singletonList(rolePermission2Mock); Mockito.doReturn(3L).when(differentPermissionsRoleMock).getId(); - Mockito.when(roleManagerImpl.findAllPermissionsBy(differentPermissionsRoleMock.getId())).thenReturn(differentPermissionsRolePermissions); + Mockito.when(roleManagerImpl.findAllRolePermissionsEntityBy(differentPermissionsRoleMock.getId())).thenReturn(differentPermissionsRolePermissions); } @Before @@ -226,7 +226,6 @@ public void removeRolesIfNeededTestRoleWithMoreAndSamePermissionsKeepRoles() { 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 +242,8 @@ 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); roles.add(callerAccountRoleMock); roles.add(morePermissionsRoleMock); @@ -261,8 +259,8 @@ 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); roles.add(callerAccountRoleMock); roles.add(differentPermissionsRoleMock); @@ -301,7 +299,7 @@ public void roleHasPermissionTestRolePermissionsDeniedApiContainRoleToAccessAllo rolePermissions.put("api1", Permission.ALLOW); rolePermissions.put("api2", Permission.DENY); - boolean result = roleManagerImpl.roleHasPermission(rolePermissions, Collections.singletonList(rolePermission1Mock)); + boolean result = roleManagerImpl.roleHasPermission(rolePermissions, List.of(rolePermission1Mock, rolePermission2Mock)); Assert.assertFalse(result); } From f5efd4e751886b53a9d9090f04489bbe6469e08a Mon Sep 17 00:00:00 2001 From: "klaus.freitas.scclouds" Date: Wed, 23 Oct 2024 19:16:34 -0300 Subject: [PATCH 22/22] fix noredist --- .../management/MockAccountManager.java | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) 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 a4f4df02a056..12c107096f76 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,8 +25,12 @@ 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.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.MoveUserCmd; import org.apache.cloudstack.api.response.UserTwoFactorAuthenticationSetupResponse; import org.apache.cloudstack.auth.UserTwoFactorAuthenticator; @@ -542,4 +546,32 @@ public void validateUserPasswordAndUpdateIfNeeded(String newPassword, UserVO use public void checkApiAccess(Account account, String command) throws PermissionDeniedException { } + + @Override + public String getAccessingApiKey (BaseCmd cmd) { + return null; + } + + @Override + public ApiKeyPair getKeyPairByApiKey(String apiKey) { + return null; + } + + @Override + public void deleteApiKey(DeleteUserKeysCmd cmd) { + } + + @Override + public void deleteApiKey(ApiKeyPair apiKeyPair) { + + } + + @Override + public List listKeyRules(ListUserKeyRulesCmd cmd) { + return null; + } + @Override + public void validateCallingUserHasAccessToDesiredUser(Long userId) { + + } }