diff --git a/api/src/main/java/com/cloud/event/EventTypes.java b/api/src/main/java/com/cloud/event/EventTypes.java index 889e821a090..6524d9de15e 100644 --- a/api/src/main/java/com/cloud/event/EventTypes.java +++ b/api/src/main/java/com/cloud/event/EventTypes.java @@ -298,8 +298,9 @@ public class EventTypes { public static final String EVENT_REGISTER_CNI_CONFIG = "REGISTER.CNI.CONFIG"; public static final String EVENT_DELETE_CNI_CONFIG = "DELETE.CNI.CONFIG"; - //register for user API and secret keys + //user API and secret keys public static final String EVENT_REGISTER_FOR_SECRET_API_KEY = "REGISTER.USER.KEY"; + public static final String EVENT_DELETE_SECRET_API_KEY = "DELETE.USER.KEY"; public static final String API_KEY_ACCESS_UPDATE = "API.KEY.ACCESS.UPDATE"; // Template Events diff --git a/api/src/main/java/com/cloud/user/AccountService.java b/api/src/main/java/com/cloud/user/AccountService.java index eb47b75ac5b..30919e5b782 100644 --- a/api/src/main/java/com/cloud/user/AccountService.java +++ b/api/src/main/java/com/cloud/user/AccountService.java @@ -21,12 +21,13 @@ import java.util.Map; import com.cloud.utils.Pair; import org.apache.cloudstack.acl.ControlledEntity; +import org.apache.cloudstack.acl.RolePermissionEntity; import org.apache.cloudstack.acl.RoleType; import org.apache.cloudstack.acl.SecurityChecker.AccessType; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPair; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPairPermission; +import org.apache.cloudstack.api.BaseCmd; import org.apache.cloudstack.api.command.admin.account.CreateAccountCmd; -import org.apache.cloudstack.api.command.admin.user.GetUserKeysCmd; -import org.apache.cloudstack.api.command.admin.user.RegisterUserKeyCmd; -import org.apache.cloudstack.api.command.admin.user.UpdateUserCmd; import com.cloud.dc.DataCenter; import com.cloud.domain.Domain; @@ -35,6 +36,14 @@ import com.cloud.network.vpc.VpcOffering; import com.cloud.offering.DiskOffering; import com.cloud.offering.NetworkOffering; import com.cloud.offering.ServiceOffering; +import org.apache.cloudstack.api.command.admin.user.DeleteUserKeysCmd; +import org.apache.cloudstack.api.command.admin.user.GetUserKeysCmd; +import org.apache.cloudstack.api.command.admin.user.ListUserKeyRulesCmd; +import org.apache.cloudstack.api.command.admin.user.ListUserKeysCmd; +import org.apache.cloudstack.api.command.admin.user.RegisterUserKeysCmd; +import org.apache.cloudstack.api.command.admin.user.UpdateUserCmd; +import org.apache.cloudstack.api.response.ApiKeyPairResponse; +import org.apache.cloudstack.api.response.ListResponse; import org.apache.cloudstack.auth.UserTwoFactorAuthenticator; import org.apache.cloudstack.backup.BackupOffering; @@ -97,7 +106,7 @@ public interface AccountService { void markUserRegistered(long userId); - public String[] createApiKeyAndSecretKey(RegisterUserKeyCmd cmd); + ApiKeyPair createApiKeyAndSecretKey(RegisterUserKeysCmd cmd); public String[] createApiKeyAndSecretKey(final long userId); @@ -125,6 +134,8 @@ public interface AccountService { void validateAccountHasAccessToResource(Account account, AccessType accessType, Object resource); + void validateCallingUserHasAccessToDesiredUser(Long userId); + Long finalizeAccountId(String accountName, Long domainId, Long projectId, boolean enabledOnly); /** @@ -134,9 +145,15 @@ public interface AccountService { */ UserAccount getUserAccountById(Long userId); - public Pair> getKeys(GetUserKeysCmd cmd); + Pair> getKeys(GetUserKeysCmd cmd); - public Pair> getKeys(Long userId); + ListResponse listKeys(ListUserKeysCmd cmd); + + List listKeyRules(ListUserKeyRulesCmd cmd); + + void deleteApiKey(DeleteUserKeysCmd cmd); + + void deleteApiKey(ApiKeyPair id); /** * Lists user two-factor authentication provider plugins @@ -151,4 +168,13 @@ public interface AccountService { */ UserTwoFactorAuthenticator getUserTwoFactorAuthenticationProvider(final Long domainId); + ApiKeyPair getLatestUserKeyPair(Long userId); + + ApiKeyPair getKeyPairById(Long id); + + ApiKeyPair getKeyPairByApiKey(String apiKey); + + String getAccessingApiKey(BaseCmd cmd); + + List getAllKeypairPermissions(String apiKey); } 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 00000000000..63405c62e32 --- /dev/null +++ b/api/src/main/java/com/cloud/user/ApiKeyPairState.java @@ -0,0 +1,21 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package com.cloud.user; + +public enum ApiKeyPairState { + ENABLED, REMOVED, EXPIRED +} diff --git a/api/src/main/java/com/cloud/user/User.java b/api/src/main/java/com/cloud/user/User.java index 041b39ad272..da7245a4798 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 interface User extends OwnedBy, InternalIdentity { public void setState(Account.State state); - public String getApiKey(); - - public void setApiKey(String apiKey); - - public String getSecretKey(); - - public void setSecretKey(String secretKey); - public String getTimezone(); public void setTimezone(String timezone); diff --git a/api/src/main/java/com/cloud/user/UserAccount.java b/api/src/main/java/com/cloud/user/UserAccount.java index e6b07fb371e..5736244e325 100644 --- a/api/src/main/java/com/cloud/user/UserAccount.java +++ b/api/src/main/java/com/cloud/user/UserAccount.java @@ -39,10 +39,6 @@ public interface UserAccount extends InternalIdentity { String getState(); - String getApiKey(); - - String getSecretKey(); - Date getCreated(); Date getRemoved(); diff --git a/api/src/main/java/org/apache/cloudstack/acl/APIChecker.java b/api/src/main/java/org/apache/cloudstack/acl/APIChecker.java index 660f64f43ef..286a3598e4f 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.exception.PermissionDeniedException; import com.cloud.user.Account; import com.cloud.user.User; import com.cloud.utils.component.Adapter; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPairPermission; import java.util.List; @@ -31,8 +32,8 @@ public interface APIChecker extends Adapter { // If true, apiChecker has checked the operation // If false, apiChecker is unable to handle the operation or not implemented // On exception, checkAccess failed don't allow - boolean checkAccess(User user, String apiCommandName) throws PermissionDeniedException; - boolean checkAccess(Account account, String apiCommandName) throws PermissionDeniedException; + boolean checkAccess(User user, String apiCommandName, ApiKeyPairPermission... apiKeyPairPermissions) throws PermissionDeniedException; + boolean checkAccess(Account account, String apiCommandName, ApiKeyPairPermission... apiKeyPairPermissions) throws PermissionDeniedException; /** * Verifies if the account has permission for the given list of APIs and returns only the allowed ones. * @@ -43,4 +44,5 @@ public interface APIChecker extends Adapter { */ List getApisAllowedToUser(Role role, User user, List apiNames) throws PermissionDeniedException; boolean isEnabled(); + List getImplicitRolePermissions(RoleType roleType); } diff --git a/api/src/main/java/org/apache/cloudstack/acl/RolePermissionEntity.java b/api/src/main/java/org/apache/cloudstack/acl/RolePermissionEntity.java index 251c6b6d3f9..f382b1c6964 100644 --- a/api/src/main/java/org/apache/cloudstack/acl/RolePermissionEntity.java +++ b/api/src/main/java/org/apache/cloudstack/acl/RolePermissionEntity.java @@ -21,7 +21,7 @@ import org.apache.cloudstack.api.Identity; import org.apache.cloudstack.api.InternalIdentity; public interface RolePermissionEntity extends InternalIdentity, Identity { - public enum Permission { + enum Permission { ALLOW, DENY } Rule getRule(); 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 f041c8342ae..14e0a608a92 100644 --- a/api/src/main/java/org/apache/cloudstack/acl/RoleService.java +++ b/api/src/main/java/org/apache/cloudstack/acl/RoleService.java @@ -104,5 +104,26 @@ public interface RoleService { List findAllPermissionsBy(Long roleId); + List findAllRolePermissionsEntityBy(Long roleId, boolean considerImplicitRules); + Permission getRolePermission(String permission); + + int removeRolesIfNeeded(List roles); + + /** + * Checks if the role of the caller account has compatible permissions of the specified role permissions. + * For each permission of the {@param rolePermissionsToAccess}, 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 RolePermissionEntity} 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 a4ef7773f67..ad01825a95f 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 @@ import org.apache.commons.lang3.StringUtils; public final class Rule { private final String rule; + private final Pattern matchingPattern; private final static Pattern ALLOWED_PATTERN = Pattern.compile("^[a-zA-Z0-9*]+$"); public Rule(final String rule) { validate(rule); this.rule = rule; + matchingPattern = Pattern.compile(rule.toLowerCase().replace("*", "(\\w*\\*?)+")); } public boolean matches(final String commandName) { - return StringUtils.isNotEmpty(commandName) - && commandName.toLowerCase().matches(rule.toLowerCase().replace("*", "\\w*")); + return StringUtils.isNotEmpty(commandName) && + matchingPattern.matcher(commandName.toLowerCase()).matches(); } public String getRuleString() { diff --git a/api/src/main/java/org/apache/cloudstack/acl/apikeypair/ApiKeyPair.java b/api/src/main/java/org/apache/cloudstack/acl/apikeypair/ApiKeyPair.java new file mode 100644 index 00000000000..ecce0ae5082 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/acl/apikeypair/ApiKeyPair.java @@ -0,0 +1,38 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.acl.apikeypair; + +import org.apache.cloudstack.acl.ControlledEntity; +import org.apache.cloudstack.api.Identity; +import org.apache.cloudstack.api.InternalIdentity; + +import java.util.Date; + +public interface ApiKeyPair extends ControlledEntity, InternalIdentity, Identity { + Long getUserId(); + Date getStartDate(); + Date getEndDate(); + Date getCreated(); + String getDescription(); + String getApiKey(); + String getSecretKey(); + String getName(); + Date getRemoved(); + void setRemoved(Date date); + void validateDate(); + boolean hasEndDatePassed(); +} diff --git a/api/src/main/java/org/apache/cloudstack/acl/apikeypair/ApiKeyPairPermission.java b/api/src/main/java/org/apache/cloudstack/acl/apikeypair/ApiKeyPairPermission.java new file mode 100644 index 00000000000..60b3834cc07 --- /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 00000000000..de9c829b17d --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/acl/apikeypair/ApiKeyPairService.java @@ -0,0 +1,27 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.acl.apikeypair; + +import java.util.List; + +public interface ApiKeyPairService { + List findAllPermissionsByKeyPairId(Long apiKeyPairId, Long roleId); + + ApiKeyPair findByApiKey(String apiKey); + + ApiKeyPair findById(Long id); +} 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 9a8913da5b0..25ba233cf80 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 @@ package org.apache.cloudstack.api; 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"; @@ -46,6 +47,7 @@ public class ApiConstants { public static final String AS_NUMBER_ID = "asnumberid"; public static final String ASN_RANGE = "asnrange"; public static final String ASN_RANGE_ID = "asnrangeid"; + public static final String API_KEY_FILTER = "apikeyfilter"; public static final String ASYNC_BACKUP = "asyncbackup"; public static final String AUTO_SELECT = "autoselect"; public static final String USER_API_KEY = "userapikey"; @@ -356,6 +358,7 @@ public class ApiConstants { public static final String JOB_STATUS = "jobstatus"; public static final String KEEPALIVE_ENABLED = "keepaliveenabled"; public static final String KERNEL_VERSION = "kernelversion"; + public static final String KEYPAIR_ID = "keypairid"; public static final String KEY = "key"; public static final String LABEL = "label"; public static final String LASTNAME = "lastname"; @@ -521,9 +524,9 @@ public class ApiConstants { public static final String SCHEDULE = "schedule"; public static final String SCHEDULE_ID = "scheduleid"; public static final String SCOPE = "scope"; + public static final String USER_SECRET_KEY = "usersecretkey"; public static final String SEARCH_BASE = "searchbase"; public static final String SECONDARY_IP = "secondaryip"; - public static final String SECRET_KEY = "secretkey"; public static final String SECURITY_GROUP_IDS = "securitygroupids"; public static final String SECURITY_GROUP_NAMES = "securitygroupnames"; public static final String SECURITY_GROUP_NAME = "securitygroupname"; @@ -540,6 +543,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"; @@ -623,7 +627,6 @@ public class ApiConstants { public static final String USERNAME = "username"; public static final String USER_CONFIGURABLE = "userconfigurable"; public static final String USER_SECURITY_GROUP_LIST = "usersecuritygrouplist"; - public static final String USER_SECRET_KEY = "usersecretkey"; public static final String USE_VIRTUAL_NETWORK = "usevirtualnetwork"; public static final String USE_VIRTUAL_ROUTER_IP_RESOLVER = "userouteripresolver"; public static final String UPDATE_IN_SEQUENCE = "updateinsequence"; @@ -765,6 +768,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"; @@ -1028,7 +1032,7 @@ public class ApiConstants { public static final String NSX_PROVIDER_PORT = "nsxproviderport"; public static final String NSX_CONTROLLER_ID = "nsxcontrollerid"; public static final String S3_ACCESS_KEY = "accesskey"; - public static final String S3_SECRET_KEY = "secretkey"; + public static final String SECRET_KEY = "secretkey"; public static final String S3_END_POINT = "endpoint"; public static final String S3_BUCKET_NAME = "bucket"; public static final String S3_SIGNER = "s3signer"; diff --git a/api/src/main/java/org/apache/cloudstack/api/BaseAsyncCmd.java b/api/src/main/java/org/apache/cloudstack/api/BaseAsyncCmd.java index 6859b0a7f40..c67c5a023e0 100644 --- a/api/src/main/java/org/apache/cloudstack/api/BaseAsyncCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/BaseAsyncCmd.java @@ -29,6 +29,7 @@ public abstract class BaseAsyncCmd extends BaseCmd { public static final String migrationSyncObject = "migration"; public static final String snapshotHostSyncObject = "snapshothost"; public static final String gslbSyncObject = "globalserverloadbalancer"; + public static final String user = "user"; private Object job; diff --git a/api/src/main/java/org/apache/cloudstack/api/BaseCmd.java b/api/src/main/java/org/apache/cloudstack/api/BaseCmd.java index f30f6f32782..e495cf28413 100644 --- a/api/src/main/java/org/apache/cloudstack/api/BaseCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/BaseCmd.java @@ -36,6 +36,7 @@ import com.cloud.bgp.BGPService; 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; @@ -221,6 +222,8 @@ public abstract class BaseCmd { @Inject public Ipv6Service ipv6Service; @Inject + public ApiKeyPairService apiKeyPairService; + @Inject public VnfTemplateManager vnfTemplateManager; @Inject public BucketApiService _bucketService; diff --git a/api/src/main/java/org/apache/cloudstack/api/ResponseGenerator.java b/api/src/main/java/org/apache/cloudstack/api/ResponseGenerator.java index 8e92e877f5c..338c1e738df 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ResponseGenerator.java +++ b/api/src/main/java/org/apache/cloudstack/api/ResponseGenerator.java @@ -24,6 +24,8 @@ import java.util.Set; import org.apache.cloudstack.api.response.ConsoleSessionResponse; import org.apache.cloudstack.consoleproxy.ConsoleSession; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPair; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPairPermission; import org.apache.cloudstack.affinity.AffinityGroup; import org.apache.cloudstack.affinity.AffinityGroupResponse; import org.apache.cloudstack.api.ApiConstants.HostDetails; @@ -41,6 +43,7 @@ import org.apache.cloudstack.api.response.AutoScaleVmProfileResponse; import org.apache.cloudstack.api.response.BackupOfferingResponse; import org.apache.cloudstack.api.response.BackupRepositoryResponse; import org.apache.cloudstack.api.response.BackupScheduleResponse; +import org.apache.cloudstack.api.response.BaseRolePermissionResponse; import org.apache.cloudstack.api.response.BucketResponse; import org.apache.cloudstack.api.response.CapacityResponse; import org.apache.cloudstack.api.response.ClusterResponse; @@ -77,6 +80,7 @@ import org.apache.cloudstack.api.response.InternalLoadBalancerElementResponse; import org.apache.cloudstack.api.response.IpForwardingRuleResponse; import org.apache.cloudstack.api.response.IpQuarantineResponse; import org.apache.cloudstack.api.response.IsolationMethodResponse; +import org.apache.cloudstack.api.response.ApiKeyPairResponse; import org.apache.cloudstack.api.response.LBHealthCheckResponse; import org.apache.cloudstack.api.response.LBStickinessResponse; import org.apache.cloudstack.api.response.ListResponse; @@ -583,4 +587,8 @@ public interface ResponseGenerator { GuiThemeResponse createGuiThemeResponse(GuiThemeJoin guiThemeJoin); ConsoleSessionResponse createConsoleSessionResponse(ConsoleSession consoleSession, ResponseView responseView); + + ApiKeyPairResponse createKeyPairResponse(ApiKeyPair keyPair); + + ListResponse createKeypairPermissionsResponse(List permissions); } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/storage/AddImageStoreS3CMD.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/storage/AddImageStoreS3CMD.java index 2fe3c7cd106..75fcf125eb1 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/storage/AddImageStoreS3CMD.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/storage/AddImageStoreS3CMD.java @@ -27,7 +27,7 @@ import static org.apache.cloudstack.api.ApiConstants.S3_END_POINT; import static org.apache.cloudstack.api.ApiConstants.S3_HTTPS_FLAG; import static org.apache.cloudstack.api.ApiConstants.S3_MAX_ERROR_RETRY; import static org.apache.cloudstack.api.ApiConstants.S3_SIGNER; -import static org.apache.cloudstack.api.ApiConstants.S3_SECRET_KEY; +import static org.apache.cloudstack.api.ApiConstants.SECRET_KEY; import static org.apache.cloudstack.api.ApiConstants.S3_SOCKET_TIMEOUT; import static org.apache.cloudstack.api.ApiConstants.S3_USE_TCP_KEEPALIVE; import static org.apache.cloudstack.api.BaseCmd.CommandType.BOOLEAN; @@ -64,7 +64,7 @@ public final class AddImageStoreS3CMD extends BaseCmd implements ClientOptions { @Parameter(name = S3_ACCESS_KEY, type = STRING, required = true, description = "S3 access key") private String accessKey; - @Parameter(name = S3_SECRET_KEY, type = STRING, required = true, description = "S3 secret key") + @Parameter(name = SECRET_KEY, type = STRING, required = true, description = "S3 secret key") private String secretKey; @Parameter(name = S3_END_POINT, type = STRING, required = true, description = "S3 endpoint") @@ -101,7 +101,7 @@ public final class AddImageStoreS3CMD extends BaseCmd implements ClientOptions { Map dm = new HashMap(); dm.put(ApiConstants.S3_ACCESS_KEY, getAccessKey()); - dm.put(ApiConstants.S3_SECRET_KEY, getSecretKey()); + dm.put(ApiConstants.SECRET_KEY, getSecretKey()); dm.put(ApiConstants.S3_END_POINT, getEndPoint()); dm.put(ApiConstants.S3_BUCKET_NAME, getBucketName()); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/user/DeleteUserKeysCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/user/DeleteUserKeysCmd.java new file mode 100644 index 00000000000..6cf55514ba3 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/user/DeleteUserKeysCmd.java @@ -0,0 +1,81 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.api.command.admin.user; + +import com.cloud.event.EventTypes; +import com.cloud.user.Account; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPair; +import org.apache.cloudstack.api.ACL; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiCommandResourceType; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseAsyncCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.response.ApiKeyPairResponse; +import org.apache.cloudstack.api.response.SuccessResponse; + +@APICommand(name = "deleteUserKeys", description = "Deletes a keypair from a user", responseObject = SuccessResponse.class, + since = "4.23.0", requestHasSensitiveInfo = false, responseHasSensitiveInfo = false) +public class DeleteUserKeysCmd extends BaseAsyncCmd { + @ACL + @Parameter(name = ApiConstants.KEYPAIR_ID, type = CommandType.UUID, entityType = ApiKeyPairResponse.class, required = true, description = "ID of the keypair to be deleted.") + private Long id; + + @Override + public ApiCommandResourceType getApiResourceType() { + return ApiCommandResourceType.User; + } + + @Override + public long getEntityOwnerId() { + ApiKeyPair keyPair = apiKeyPairService.findById(id); + if (keyPair != null) { + return keyPair.getAccountId(); + } + return Account.ACCOUNT_ID_SYSTEM; + } + + public Long getId() { + return id; + } + + @Override + public void execute() { + _accountService.deleteApiKey(this); + + SuccessResponse response = new SuccessResponse(getCommandName()); + this.setResponseObject(response); + } + + @Override + public String getEventType() { + return EventTypes.EVENT_DELETE_SECRET_API_KEY; + } + + @Override + public String getEventDescription() { + ApiKeyPair keyPair = apiKeyPairService.findById(id); + return String.format("Deleting API key pair with ID [%s]%s", + keyPair == null ? id : keyPair.getUuid(), + keyPair == null ? "." : String.format(" and name [%s].", keyPair.getName())); + } + + @Override + public Long getSyncObjId() { + return getId(); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/user/GetUserKeysCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/user/GetUserKeysCmd.java index f04c5f6c478..a651befcff7 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/user/GetUserKeysCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/user/GetUserKeysCmd.java @@ -32,33 +32,33 @@ import org.apache.cloudstack.api.response.UserResponse; import java.util.Map; @APICommand(name = "getUserKeys", - description = "This command allows the user to query the seceret and API keys for the account", - responseObject = RegisterUserKeyResponse.class, - requestHasSensitiveInfo = false, - responseHasSensitiveInfo = true, - authorized = {RoleType.User, RoleType.Admin, RoleType.DomainAdmin, RoleType.ResourceAdmin}, - since = "4.10.0") - -public class GetUserKeysCmd extends BaseCmd{ - - @Parameter(name= ApiConstants.ID, type = CommandType.UUID, entityType = UserResponse.class, required = true, description = "ID of the user whose keys are required") + description = "Queries the last registered secret and API keys of a user.", + responseObject = RegisterUserKeyResponse.class, + requestHasSensitiveInfo = false, + responseHasSensitiveInfo = true, + authorized = {RoleType.User, RoleType.Admin, RoleType.DomainAdmin, RoleType.ResourceAdmin}, + since = "4.10.0") +public class GetUserKeysCmd extends BaseCmd { + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = UserResponse.class, required = true, description = "ID of the user whose keys are required") private Long id; - - public Long getID(){ + public Long getId() { return id; - }public long getEntityOwnerId(){ - User user = _entityMgr.findById(User.class, getID()); - if(user != null){ + } + + public long getEntityOwnerId() { + User user = _entityMgr.findById(User.class, getId()); + if (user != null) { return user.getAccountId(); } - else return Account.ACCOUNT_ID_SYSTEM; + return Account.ACCOUNT_ID_SYSTEM; } - public void execute(){ + + public void execute() { Pair> keys = _accountService.getKeys(this); RegisterUserKeyResponse response = new RegisterUserKeyResponse(); - if(keys != null){ + if (keys != null){ response.setApiKeyAccess(keys.first()); response.setApiKey(keys.second().get("apikey")); response.setSecretKey(keys.second().get("secretkey")); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/user/ListUserKeyRulesCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/user/ListUserKeyRulesCmd.java new file mode 100644 index 00000000000..4345cae9291 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/user/ListUserKeyRulesCmd.java @@ -0,0 +1,68 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.api.command.admin.user; + + +import com.cloud.exception.InsufficientCapacityException; +import com.cloud.exception.ResourceUnavailableException; +import com.cloud.user.Account; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPair; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPairPermission; +import org.apache.cloudstack.api.ACL; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.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; + +import java.util.List; + +@APICommand(name = "listUserKeyRules", + description = "Lists the rules defined for a API key pair.", + responseObject = BaseRolePermissionResponse.class, + requestHasSensitiveInfo = false, + responseHasSensitiveInfo = false, + since = "4.23.0") +public class ListUserKeyRulesCmd extends BaseCmd { + @ACL + @Parameter(name = ApiConstants.KEYPAIR_ID, type = CommandType.UUID, entityType = ApiKeyPairResponse.class, description = "ID of the key pair.", required = true) + private Long id; + + public Long getId() { + return id; + } + + public long getEntityOwnerId() { + ApiKeyPair keyPair = apiKeyPairService.findById(getId()); + if (keyPair != null) { + return keyPair.getAccountId(); + } + return Account.ACCOUNT_ID_SYSTEM; + } + + @Override + public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException { + List permissions = _accountService.listKeyRules(this); + ListResponse response = _responseGenerator.createKeypairPermissionsResponse(permissions); + response.setResponseName(getCommandName()); + setResponseObject(response); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/user/ListUserKeysCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/user/ListUserKeysCmd.java new file mode 100644 index 00000000000..ded05d6381a --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/user/ListUserKeysCmd.java @@ -0,0 +1,101 @@ +// 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.BaseListCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.api.response.ApiKeyPairResponse; +import org.apache.cloudstack.api.response.UserResponse; + +@APICommand(name = "listUserKeys", + description = "Lists the API key pairs (API and secret keys) of a user.", + responseObject = ApiKeyPairResponse.class, + requestHasSensitiveInfo = false, + responseHasSensitiveInfo = true, + authorized = {RoleType.User, RoleType.Admin, RoleType.DomainAdmin, RoleType.ResourceAdmin}, + since = "4.23.0") +public class ListUserKeysCmd extends BaseListCmd { + @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 key pair.") + private Long keyPairId; + + @Parameter(name = ApiConstants.API_KEY_FILTER, type = CommandType.STRING, description = "API key of the key pair.") + private String apiKeyFilter; + + @Parameter(name = ApiConstants.SHOW_PERMISSIONS, type = CommandType.BOOLEAN, description = "Whether API Key rules should be returned. Defaults to false.") + private boolean showPermissions = false; + + @Parameter(name = ApiConstants.LIST_ALL, type = CommandType.BOOLEAN, authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin}, + description = "Lists all API key pairs of users that are accessible by the calling account. Only available for administrators. Defaults to false.") + private boolean listAll = false; + + public Long getUserId() { + return userId; + } + + public Long getKeyId() { + return keyPairId; + } + + public String getApiKeyFilter() { + return apiKeyFilter; + } + + public Boolean getShowPermissions() { + return showPermissions; + } + + public boolean getListAll() { + return listAll; + } + + public long getEntityOwnerId() { + if (getKeyId() != null) { + ApiKeyPair keypair = apiKeyPairService.findById(getKeyId()); + if (keypair != null) { + return keypair.getAccountId(); + } + } else if (getUserId() != null) { + UserAccount userAccount = _accountService.getUserAccountById(getUserId()); + if (userAccount != null) { + return userAccount.getAccountId(); + } + } + return Account.ACCOUNT_ID_SYSTEM; + } + + public void execute() { + ListResponse finalResponse = _accountService.listKeys(this); + finalResponse.setObjectName("userkeys"); + finalResponse.setResponseName(getCommandName()); + setResponseObject(finalResponse); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/user/RegisterUserKeyCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/user/RegisterUserKeyCmd.java deleted file mode 100644 index 61f47e2799c..00000000000 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/user/RegisterUserKeyCmd.java +++ /dev/null @@ -1,93 +0,0 @@ -// Licensed to the Apache Software Foundation (ASF) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The ASF licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. -package org.apache.cloudstack.api.command.admin.user; - -import org.apache.cloudstack.api.ApiCommandResourceType; - -import org.apache.cloudstack.api.APICommand; -import org.apache.cloudstack.api.ApiConstants; -import org.apache.cloudstack.api.BaseCmd; -import org.apache.cloudstack.api.Parameter; -import org.apache.cloudstack.api.response.RegisterUserKeyResponse; -import org.apache.cloudstack.api.response.UserResponse; - -import com.cloud.user.Account; -import com.cloud.user.User; - -@APICommand(name = "registerUserKeys", - responseObject = RegisterUserKeyResponse.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 RegisterUserKeyCmd extends BaseCmd { - - - ///////////////////////////////////////////////////// - //////////////// API parameters ///////////////////// - ///////////////////////////////////////////////////// - - @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = UserResponse.class, required = true, description = "User id") - private Long id; - - ///////////////////////////////////////////////////// - /////////////////// Accessors /////////////////////// - ///////////////////////////////////////////////////// - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - ///////////////////////////////////////////////////// - /////////////// API Implementation/////////////////// - ///////////////////////////////////////////////////// - - @Override - public long getEntityOwnerId() { - User user = _entityMgr.findById(User.class, getId()); - if (user != null) { - return user.getAccountId(); - } - - return Account.ACCOUNT_ID_SYSTEM; // no account info given, parent this command to SYSTEM so ERROR events are tracked - } - - @Override - public Long getApiResourceId() { - return id; - } - - @Override - public ApiCommandResourceType getApiResourceType() { - return ApiCommandResourceType.User; - } - - @Override - public void execute() { - String[] keys = _accountService.createApiKeyAndSecretKey(this); - RegisterUserKeyResponse response = new RegisterUserKeyResponse(); - if (keys != null) { - response.setApiKey(keys[0]); - response.setSecretKey(keys[1]); - } - response.setObjectName("userkeys"); - response.setResponseName(getCommandName()); - this.setResponseObject(response); - } -} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/user/RegisterUserKeysCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/user/RegisterUserKeysCmd.java new file mode 100644 index 00000000000..11d7c1d2ffa --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/user/RegisterUserKeysCmd.java @@ -0,0 +1,209 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.api.command.admin.user; + +import com.cloud.event.EventTypes; +import com.cloud.user.Account; +import com.cloud.user.User; +import org.apache.cloudstack.acl.Rule; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPair; +import org.apache.cloudstack.api.ApiCommandResourceType; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseAsyncCmd; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.commons.lang3.StringUtils; + +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.response.ApiKeyPairResponse; +import org.apache.cloudstack.api.response.UserResponse; + +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@APICommand(name = "registerUserKeys", + responseObject = ApiKeyPairResponse.class, + description = "Registers an API key pair (API and secret keys) for a user.", + requestHasSensitiveInfo = false, responseHasSensitiveInfo = true) +public class RegisterUserKeysCmd extends BaseAsyncCmd { + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = UserResponse.class, required = true, description = "ID of the user.") + private Long id; + + @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, description = "API key pair name.") + private String name; + + @Parameter(name = ApiConstants.DESCRIPTION, type = CommandType.STRING, description = "API key pair description.") + private String description; + + @Parameter(name = ApiConstants.START_DATE, type = CommandType.DATE, description = "Start date of the API key pair. " + + ApiConstants.PARAMETER_DESCRIPTION_START_DATE_POSSIBLE_FORMATS) + private Date startDate; + + @Parameter(name = ApiConstants.END_DATE, type = CommandType.DATE, description = "Expiration date of the API key pair. " + + ApiConstants.PARAMETER_DESCRIPTION_END_DATE_POSSIBLE_FORMATS) + private Date endDate; + + @Parameter(name = ApiConstants.RULES, type = CommandType.MAP, description = "The rules of the API key pair. If no rules are informed, " + + "defaults to allowing all account permissions. Otherwise, only the explicitly informed permissions for the key pair will be " + + "considered. Lower indexed rules take precedence over higher. Thus, in the following example: " + + "\"rules[0].rule=deleteUserKeys rules[0].permission=deny rules[1].rule=*UserKey* rules[1].permission=allow\", all rules matching " + + "the expression \"*UserKeys*\" will be allowed, except for \"deleteUserKeys\".") + private Map rules; + + public void setUserId(Long userId) { + this.id = userId; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public Date getStartDate() { + return startDate; + } + + public void setStartDate(Date startDate) { + this.startDate = startDate; + } + + public Date getEndDate() { + return endDate; + } + + public void setEndDate(Date endDate) { + this.endDate = endDate; + } + + public void setRules(Map rules) { + this.rules = rules; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + public List> getRules() { + List> rulesDetails = new ArrayList<>(); + + if (rules == null) { + return rulesDetails; + } + + for (Object ruleObject : rules.values()) { + HashMap detail = (HashMap) ruleObject; + Map ruleDetails = new HashMap<>(); + + String rule = detail.get(ApiConstants.RULE); + if (StringUtils.isEmpty(rule)) { + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Empty rule has been provided in the [rules] parameter."); + } + + 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.RULE, new Rule(rule)); + ruleDetails.put(ApiConstants.PERMISSION, roleService.getRolePermission(permission)); + + String description = detail.get(ApiConstants.DESCRIPTION); + if (StringUtils.isNotEmpty(description)) { + ruleDetails.put(ApiConstants.DESCRIPTION, description); + } + + rulesDetails.add(ruleDetails); + } + return rulesDetails; + } + + @Override + public long getEntityOwnerId() { + User user = _entityMgr.findById(User.class, getUserId()); + List accessibleUsers = _queryService.searchForAccessibleUsers(); + if (user != null && accessibleUsers.stream().anyMatch(u -> u == user.getId())) { + return user.getAccountId(); + } + return Account.ACCOUNT_ID_SYSTEM; + } + + public Long getUserId() { + return id; + } + + @Override + public Long getApiResourceId() { + User user = _entityMgr.findById(User.class, getUserId()); + if (user != null) { + return user.getId(); + } + return null; + } + + @Override + public ApiCommandResourceType getApiResourceType() { + return ApiCommandResourceType.User; + } + + @Override + public void execute() { + ApiKeyPair apiKeyPair = _accountService.createApiKeyAndSecretKey(this); + ApiKeyPairResponse response = new ApiKeyPairResponse(); + if (apiKeyPair != null) { + response = _responseGenerator.createKeyPairResponse(apiKeyPair); + } + response.setObjectName("userkeys"); + response.setResponseName(getCommandName()); + this.setResponseObject(response); + } + + @Override + public String getEventType() { + return EventTypes.EVENT_REGISTER_FOR_SECRET_API_KEY; + } + + @Override + public String getEventDescription() { + String userUuid = getResourceUuid(ApiConstants.ID); + return String.format("Registering API keypair for user [%s].", userUuid == null ? id : userUuid); + } + + @Override + public String getSyncObjType() { + return BaseAsyncCmd.user; + } + + @Override + public Long getSyncObjId() { + return getUserId(); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/user/UpdateUserCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/user/UpdateUserCmd.java index 628ddb96deb..3f5ce241502 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/user/UpdateUserCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/user/UpdateUserCmd.java @@ -46,8 +46,8 @@ public class UpdateUserCmd extends BaseCmd { //////////////// API parameters ///////////////////// ///////////////////////////////////////////////////// - @Parameter(name = ApiConstants.USER_API_KEY, type = CommandType.STRING, description = "The API key for the user. Must be specified with userSecretKey") - private String apiKey; + @Parameter(name = ApiConstants.USER_API_KEY, type = CommandType.STRING, description = "Updates the latest API key of the user. Must be specified with usersecretkey") + private String userApiKey; @Parameter(name = ApiConstants.EMAIL, type = CommandType.STRING, description = "Email") private String email; @@ -70,8 +70,8 @@ public class UpdateUserCmd extends BaseCmd { @Parameter(name = ApiConstants.CURRENT_PASSWORD, type = CommandType.STRING, description = "Current password that was being used by the user. You must inform the current password when updating the password.", acceptedOnAdminPort = false) private String currentPassword; - @Parameter(name = ApiConstants.USER_SECRET_KEY, type = CommandType.STRING, description = "The secret key for the user. Must be specified with userApiKey") - private String secretKey; + @Parameter(name = ApiConstants.USER_SECRET_KEY, type = CommandType.STRING, description = "Updates the latest secret key of the user. Must be specified with userapikey.") + private String userSecretKey; @Parameter(name = ApiConstants.API_KEY_ACCESS, type = CommandType.STRING, description = "Determines if Api key access for this user is enabled, disabled or inherits the value from its parent, the owning account", since = "4.20.1.0", authorized = {RoleType.Admin}) private String apiKeyAccess; @@ -99,7 +99,7 @@ public class UpdateUserCmd extends BaseCmd { ///////////////////////////////////////////////////// public String getApiKey() { - return apiKey; + return userApiKey; } public String getEmail() { @@ -127,7 +127,7 @@ public class UpdateUserCmd extends BaseCmd { } public String getSecretKey() { - return secretKey; + return userSecretKey; } public String getApiKeyAccess() { diff --git a/api/src/main/java/org/apache/cloudstack/api/response/ApiKeyPairResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/ApiKeyPairResponse.java new file mode 100644 index 00000000000..350d71b3788 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/response/ApiKeyPairResponse.java @@ -0,0 +1,285 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.api.response; + +import com.cloud.user.ApiKeyPairState; +import com.google.gson.annotations.SerializedName; + +import java.util.Date; +import java.util.List; + +import org.apache.cloudstack.acl.apikeypair.ApiKeyPair; +import org.apache.cloudstack.api.ApiConstants; + +import com.cloud.serializer.Param; +import org.apache.cloudstack.api.BaseResponseWithAnnotations; +import org.apache.cloudstack.api.EntityReference; + +@EntityReference(value = ApiKeyPair.class) +public class ApiKeyPairResponse extends BaseResponseWithAnnotations { + @SerializedName(ApiConstants.NAME) + @Param(description = "Name of the API key pair") + private String name; + + @SerializedName(ApiConstants.API_KEY) + @Param(description = "The API key of the registered user.", isSensitive = true) + private String userApiKey; + + @SerializedName(ApiConstants.SECRET_KEY) + @Param(description = "The secret key of the registered user.", isSensitive = true) + private String userSecretKey; + + @SerializedName(ApiConstants.USER_ID) + @Param(description = "ID of the user that owns the keypair.") + private String userId; + + @SerializedName(ApiConstants.USERNAME) + @Param(description = "Username of the keypair's owner.") + private String username; + + @SerializedName(ApiConstants.ID) + @Param(description = "ID of the API key pair.", isSensitive = true) + private String id; + + @SerializedName(ApiConstants.DESCRIPTION) + @Param(description = "API key pair description.") + private String description; + + @SerializedName(ApiConstants.START_DATE) + @Param(description = "API key pair start date.") + private Date startDate; + + @SerializedName(ApiConstants.END_DATE) + @Param(description = "API key pair expiration date.") + private Date endDate; + + @SerializedName(ApiConstants.CREATED) + @Param(description = "API key pair creation timestamp.") + private Date created; + + @SerializedName(ApiConstants.ACCOUNT_TYPE) + @Param(description = "Account type.") + private String accountType; + + @SerializedName(ApiConstants.ACCOUNT_ID) + @Param(description = "Account ID.") + private String accountId; + + @SerializedName(ApiConstants.ACCOUNT_NAME) + @Param(description = "Account name.") + private String accountName; + + @SerializedName(ApiConstants.ROLE_ID) + @Param(description = "ID of the role.") + private String roleId; + + @SerializedName(ApiConstants.ROLE_TYPE) + @Param(description = "Type of the role (Admin, ResourceAdmin, DomainAdmin, User).") + private String roleType; + + @SerializedName(ApiConstants.ROLE_NAME) + @Param(description = "Name of the role.") + private String roleName; + + @SerializedName(ApiConstants.PERMISSIONS) + @Param(description = "Permissions of the API key pair.") + private List permissions; + + @SerializedName(ApiConstants.DOMAIN_ID) + @Param(description = "ID of the domain which the account belongs to.") + private String domainId; + + @SerializedName(ApiConstants.DOMAIN) + @Param(description = "Name of the domain which the account belongs to.") + private String domainName; + + @SerializedName(ApiConstants.DOMAIN_PATH) + @Param(description = "Path of the domain which the account belongs to.") + private String domainPath; + + @SerializedName(ApiConstants.STATE) + @Param(description = "State of the API key pair.") + private ApiKeyPairState state; + + public String getApiKey() { + return userApiKey; + } + + public void setApiKey(String apiKey) { + this.userApiKey = apiKey; + } + + public String getSecretKey() { + return userSecretKey; + } + + public void setSecretKey(String secretKey) { + this.userSecretKey = secretKey; + } + + public String getId() { + return this.id; + } + + public void setId(String id) { + this.id = id; + } + + public String getUserId() { + return userId; + } + + public void setUserId(String userId) { + this.userId = userId; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public Date getStartDate() { + return startDate; + } + + public void setStartDate(Date startDate) { + this.startDate = startDate; + } + + public Date getEndDate() { + return endDate; + } + + public void setEndDate(Date endDate) { + this.endDate = endDate; + } + + public Date getCreated() { + return created; + } + + public void setCreated(Date created) { + this.created = created; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getAccountType() { + return accountType; + } + + public void setAccountType(String accountType) { + this.accountType = accountType; + } + + public String getRoleId() { + return roleId; + } + + public void setRoleId(String roleId) { + this.roleId = roleId; + } + + public String getAccountId() { + return accountId; + } + + public void setAccountId(String accountId) { + this.accountId = accountId; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getRoleType() { + return roleType; + } + + public void setRoleType(String roleType) { + this.roleType = roleType; + } + + public String getRoleName() { + return roleName; + } + + public void setRoleName(String roleName) { + this.roleName = roleName; + } + + public String getDomainId() { + return domainId; + } + + public void setDomainId(String domainId) { + this.domainId = domainId; + } + + public String getDomainName() { + return domainName; + } + + public void setDomainName(String domainName) { + this.domainName = domainName; + } + + public String getDomainPath() { + return domainPath; + } + + public void setDomainPath(String domainPath) { + this.domainPath = domainPath; + } + + public ApiKeyPairState getState() { + return state; + } + + public void setState(ApiKeyPairState state) { + this.state = state; + } + + public String getAccountName() { + return accountName; + } + + public void setAccountName(String accountName) { + this.accountName = accountName; + } + + public List getPermissions() { + return permissions; + } + + public void setPermissions(List permissions) { + this.permissions = permissions; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/response/UserResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/UserResponse.java index 3b1c1a10885..61b025b206e 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/UserResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/UserResponse.java @@ -95,12 +95,12 @@ public class UserResponse extends BaseResponse implements SetResourceIconRespons @Param(description = "The timezone user was created in") private String timezone; - @SerializedName("apikey") + @SerializedName(ApiConstants.API_KEY) @Param(description = "The API key of the user", isSensitive = true) private String apiKey; @Deprecated - @SerializedName("secretkey") + @SerializedName(ApiConstants.SECRET_KEY) @Param(description = "The secret key of the user", isSensitive = true) private String secretKey; 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 5cd67ffe9ba..5b053aafd84 100644 --- a/api/src/main/java/org/apache/cloudstack/query/QueryService.java +++ b/api/src/main/java/org/apache/cloudstack/query/QueryService.java @@ -145,6 +145,8 @@ public interface QueryService { ListResponse searchForUsers(Long domainId, boolean recursive) throws PermissionDeniedException; + List searchForAccessibleUsers(); + ListResponse searchForEvents(ListEventsCmd cmd); ListResponse listTags(ListTagsCmd cmd); diff --git a/api/src/test/java/org/apache/cloudstack/acl/RuleTest.java b/api/src/test/java/org/apache/cloudstack/acl/RuleTest.java index 79e6127d29a..b99ba48c66d 100644 --- a/api/src/test/java/org/apache/cloudstack/acl/RuleTest.java +++ b/api/src/test/java/org/apache/cloudstack/acl/RuleTest.java @@ -17,13 +17,46 @@ package org.apache.cloudstack.acl; import com.cloud.exception.InvalidParameterValueException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Pattern; +import org.apache.cloudstack.api.APICommand; import org.junit.Assert; +import org.junit.BeforeClass; import org.junit.Test; import java.util.Arrays; +import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; +import org.springframework.core.type.filter.AnnotationTypeFilter; public class RuleTest { + private static List apiNames; + private static List apiRules; + private static ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false); + + @BeforeClass + public static void setup() { + provider.addIncludeFilter(new AnnotationTypeFilter(APICommand.class)); + Set beanDefinitions = provider.findCandidateComponents("org.apache.cloudstack.api"); + + apiNames = new ArrayList<>(); + apiRules = new ArrayList<>(); + for(BeanDefinition bd : beanDefinitions) { + if (bd instanceof AnnotatedBeanDefinition) { + Map annotationAttributeMap = ((AnnotatedBeanDefinition) bd).getMetadata() + .getAnnotationAttributes(APICommand.class.getName()); + String apiName = annotationAttributeMap.get("name").toString(); + apiNames.add(apiName); + apiRules.add(new Rule(apiName)); + } + } + } + @Test public void testToString() throws Exception { Rule rule = new Rule("someString"); @@ -31,21 +64,89 @@ public class RuleTest { } @Test - public void testMatchesEmpty() throws Exception { - Rule rule = new Rule("someString"); - Assert.assertFalse(rule.matches("")); + public void ruleMatchesTestNoMatchesOnEmptyString() throws Exception { + String testCmd = ""; + List matches = new ArrayList<>(); + for (Rule rule : apiRules) { + if (rule.matches(testCmd)) { + matches.add(rule.getRuleString()); + } + } + + Assert.assertEquals(matches.size(), 0); } @Test - public void testMatchesNull() throws Exception { - Rule rule = new Rule("someString"); - Assert.assertFalse(rule.matches(null)); + public void ruleMatchesTestNoMatchesOnNull() throws Exception { + List matches = new ArrayList<>(); + for (Rule rule : apiRules) { + if (rule.matches(null)) { + matches.add(rule.getRuleString()); + } + } + + Assert.assertTrue(matches.isEmpty()); } @Test - public void testMatchesSpace() throws Exception { - Rule rule = new Rule("someString"); - Assert.assertFalse(rule.matches(" ")); + public void ruleMatchesTestNoMatchesOnSpaceCharacter() throws Exception { + String testCmd = " "; + List matches = new ArrayList<>(); + for (Rule rule : apiRules) { + if (rule.matches(testCmd)) { + matches.add(rule.getRuleString()); + } + } + + Assert.assertTrue(matches.isEmpty()); + } + + @Test + public void ruleMatchesTestWildCardOnEndWorksAsNormalRegex() { + setup(); + Pattern regexPattern = Pattern.compile("list.*"); + Rule acsRegexRule = new Rule("list*"); + + List nonMatches = new ArrayList<>(); + for (String apiName : apiNames) { + if (acsRegexRule.matches(apiName) != regexPattern.matcher(apiName).matches()) { + nonMatches.add(apiName); + } + } + + Assert.assertTrue(nonMatches.isEmpty()); + } + + @Test + public void ruleMatchesTestWildCardOnMiddleWorksAsNormalRegex() { + setup(); + Pattern regexPattern = Pattern.compile("list.*s"); + Rule acsRegexRule = new Rule("list*s"); + + List nonMatches = new ArrayList<>(); + for (String apiName : apiNames) { + if (acsRegexRule.matches(apiName) != regexPattern.matcher(apiName).matches()) { + nonMatches.add(apiName); + } + } + + Assert.assertTrue(nonMatches.isEmpty()); + } + + @Test + public void ruleMatchesTestWildCardOnStartWorksAsNormalRegex() { + setup(); + Pattern regexPattern = Pattern.compile(".*User"); + Rule acsRegexRule = new Rule("*User"); + + List nonMatches = new ArrayList<>(); + for (String apiName : apiNames) { + if (acsRegexRule.matches(apiName) != regexPattern.matcher(apiName).matches()) { + nonMatches.add(apiName); + } + } + + Assert.assertTrue(nonMatches.isEmpty()); } @Test @@ -73,7 +174,25 @@ public class RuleTest { } @Test - public void testValidateRuleWithValidData() throws Exception { + public void ruleMatchesTestWildcardOnRuleAndCommand() throws Exception { + Rule rule = new Rule("*"); + Assert.assertTrue(rule.matches("list*")); + } + + @Test + public void ruleMatchesTestWildcardOnRuleAndCommandNotAllowed() throws Exception { + Rule rule = new Rule("list*"); + Assert.assertFalse(rule.matches("*")); + } + + @Test + public void ruleMatchesTestWithMultipleStars() throws Exception { + Rule rule = new Rule("list***"); + Assert.assertFalse(rule.matches("api")); + } + + @Test + public void testRuleToStringWithValidStrings() throws Exception { for (String rule : Arrays.asList("a", "1", "someApi", "someApi321", "123SomeApi", "prefix*", "*middle*", "*Suffix", "*", "**", "f***", "m0nk3yMa**g1c*")) { @@ -82,7 +201,7 @@ public class RuleTest { } @Test - public void testValidateRuleWithInvalidData() throws Exception { + public void testRuleToStringWithInvalidStrings() throws Exception { for (String rule : Arrays.asList(null, "", " ", " ", "\n", "\t", "\r", "\"", "\'", "^someApi$", "^someApi", "some$", "some-Api;", "some,Api", "^", "$", "^$", ".*", "\\w+", "r**l3rd0@Kr3", "j@s1n|+|0È·", diff --git a/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade410to420.java b/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade410to420.java index a78f93fbdd4..5c47087b968 100644 --- a/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade410to420.java +++ b/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade410to420.java @@ -1947,7 +1947,7 @@ public class Upgrade410to420 extends DbUpgradeAbstractImpl { Map detailMap = new HashMap(); detailMap.put(ApiConstants.S3_ACCESS_KEY, s3_accesskey); - detailMap.put(ApiConstants.S3_SECRET_KEY, s3_secretkey); + detailMap.put(ApiConstants.SECRET_KEY, s3_secretkey); detailMap.put(ApiConstants.S3_BUCKET_NAME, s3_bucket); detailMap.put(ApiConstants.S3_END_POINT, s3_endpoint); detailMap.put(ApiConstants.S3_HTTPS_FLAG, String.valueOf(s3_https)); diff --git a/engine/schema/src/main/java/com/cloud/user/UserAccountVO.java b/engine/schema/src/main/java/com/cloud/user/UserAccountVO.java index c5ca410fc53..7345eeb4853 100644 --- a/engine/schema/src/main/java/com/cloud/user/UserAccountVO.java +++ b/engine/schema/src/main/java/com/cloud/user/UserAccountVO.java @@ -36,7 +36,6 @@ import org.apache.cloudstack.api.InternalIdentity; import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; import org.apache.commons.lang3.StringUtils; -import com.cloud.utils.db.Encrypt; import com.cloud.utils.db.GenericDao; @Entity @@ -69,13 +68,6 @@ public class UserAccountVO implements UserAccount, InternalIdentity { @Column(name = "state") private String state; - @Column(name = "api_key") - private String apiKey = null; - - @Encrypt - @Column(name = "secret_key") - private String secretKey = null; - @Column(name = GenericDao.CREATED_COLUMN) private Date created; @@ -203,24 +195,6 @@ public class UserAccountVO implements UserAccount, InternalIdentity { 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 d74aa7ed41b..1b89bc215cf 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.api.InternalIdentity; import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; import com.cloud.user.Account.State; -import com.cloud.utils.db.Encrypt; import com.cloud.utils.db.GenericDao; import org.apache.commons.lang3.StringUtils; @@ -71,13 +70,6 @@ public class UserVO implements User, Identity, InternalIdentity { @Enumerated(value = EnumType.STRING) private State state; - @Column(name = "api_key") - private String apiKey = null; - - @Encrypt - @Column(name = "secret_key") - private String secretKey = null; - @Column(name = GenericDao.CREATED_COLUMN) private Date created; @@ -150,8 +142,6 @@ public class UserVO implements User, Identity, InternalIdentity { this.setTimezone(user.getTimezone()); this.setUuid(user.getUuid()); this.setSource(user.getSource()); - this.setApiKey(user.getApiKey()); - this.setSecretKey(user.getSecretKey()); this.setExternalEntity(user.getExternalEntity()); this.setRegistered(user.isRegistered()); this.setRegistrationToken(user.getRegistrationToken()); @@ -243,26 +233,6 @@ public class UserVO implements User, Identity, InternalIdentity { 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/UserAccountDao.java b/engine/schema/src/main/java/com/cloud/user/dao/UserAccountDao.java index de3b769571e..e377bbab94e 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 c9de9a367ee..28392abbff5 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 @@ package com.cloud.user.dao; import com.cloud.user.UserAccount; import com.cloud.user.UserAccountVO; import com.cloud.utils.db.GenericDaoBase; -import com.cloud.utils.db.SearchBuilder; import com.cloud.utils.db.SearchCriteria; import org.springframework.stereotype.Component; @@ -28,14 +27,6 @@ import java.util.List; @Component public class UserAccountDaoImpl extends GenericDaoBase implements UserAccountDao { - protected final SearchBuilder userAccountSearch; - - public UserAccountDaoImpl() { - userAccountSearch = createSearchBuilder(); - userAccountSearch.and("apiKey", userAccountSearch.entity().getApiKey(), SearchCriteria.Op.EQ); - userAccountSearch.done(); - } - @Override public List getAllUsersByNameAndEntity(String username, String entity) { if (username == null) { @@ -79,12 +70,4 @@ public class UserAccountDaoImpl extends GenericDaoBase impl } 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 14b07425150..2e160efb950 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 8baf732c240..de60e48dff8 100644 --- a/engine/schema/src/main/java/com/cloud/user/dao/UserDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/user/dao/UserDaoImpl.java @@ -65,10 +65,6 @@ public class UserDaoImpl extends GenericDaoBase implements UserDao UserIdSearch.and("id", UserIdSearch.entity().getId(), SearchCriteria.Op.EQ); UserIdSearch.done(); - SecretKeySearch = createSearchBuilder(); - SecretKeySearch.and("secretKey", SecretKeySearch.entity().getSecretKey(), SearchCriteria.Op.EQ); - SecretKeySearch.done(); - RegistrationTokenSearch = createSearchBuilder(); RegistrationTokenSearch.and("registrationToken", RegistrationTokenSearch.entity().getRegistrationToken(), SearchCriteria.Op.EQ); RegistrationTokenSearch.done(); @@ -121,13 +117,6 @@ public class UserDaoImpl extends GenericDaoBase implements UserDao 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 00000000000..7972fe6bc62 --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/acl/ApiKeyPairPermissionVO.java @@ -0,0 +1,61 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.acl; + +import org.apache.cloudstack.acl.apikeypair.ApiKeyPairPermission; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; + +@Entity +@Table(name = "api_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 ApiKeyPairPermissionVO(String rule, Permission permission, String description) { + super(rule, permission, description); + } + + public ApiKeyPairPermissionVO() { + } + + public long getApiKeyPairId() { + return this.apiKeyPairId; + } + + public void setApiKeyPairId(long keyPairId) { + this.apiKeyPairId = keyPairId; + } + + 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 00000000000..eb38b08f615 --- /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.InvalidParameterValueException; +import com.cloud.user.Account; +import com.cloud.utils.db.Encrypt; +import java.time.Instant; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPair; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; +import java.util.Date; +import java.util.Objects; +import java.util.UUID; +import org.joda.time.DateTime; + +@Entity +@Table(name = "api_keypair") +public class ApiKeyPairVO implements ApiKeyPair { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id", nullable = false) + private Long id; + + @Column(name = "uuid", nullable = false) + private String uuid = UUID.randomUUID().toString(); + + @Column(name = "user_id", nullable = false) + private Long userId; + + @Column(name = "name", nullable = false) + private String name; + + @Column(name = "domain_id", nullable = false) + private Long domainId; + + @Column(name = "account_id", nullable = false) + private Long accountId; + + @Column(name = "start_date") + @Temporal(value = TemporalType.TIMESTAMP) + private Date startDate; + + @Column(name = "end_date") + @Temporal(value = TemporalType.TIMESTAMP) + private Date endDate; + + @Column(name = "created", nullable = false) + @Temporal(value = TemporalType.TIMESTAMP) + private Date created = Date.from(Instant.now()); + + @Column(name = "description") + private String description = ""; + + @Column(name = "api_key", nullable = false) + private String apiKey; + + @Encrypt + @Column(name = "secret_key", nullable = false) + private String secretKey; + + @Column(name = "removed") + @Temporal(value = TemporalType.TIMESTAMP) + private Date removed; + + public ApiKeyPairVO() { + } + + public ApiKeyPairVO(Long id) { + this.id = id; + } + + public ApiKeyPairVO(Long userId, String description, Date startDate, Date endDate, + String apiKey, String secretKey) { + this.userId = userId; + this.description = description; + this.startDate = startDate; + this.endDate = endDate; + this.apiKey = apiKey; + this.secretKey = secretKey; + } + + public ApiKeyPairVO(String name, Long userId, String description, Date startDate, Date endDate, Account account) { + this.name = Objects.requireNonNullElseGet(name, () -> userId + " - API key pair"); + this.userId = userId; + this.description = description; + this.startDate = startDate; + this.endDate = endDate; + this.domainId = account.getDomainId(); + this.accountId = account.getAccountId(); + } + + public ApiKeyPairVO(Long id, Long userId) { + this.id = id; + this.userId = userId; + } + + public void validateDate() { + Date now = DateTime.now().toDate(); + Date keypairStart = this.getStartDate(); + Date keypairExpiration = this.getEndDate(); + if (keypairStart != null && now.compareTo(keypairStart) <= 0) { + throw new InvalidParameterValueException(String.format("API key pair is not valid yet, start date: %s", keypairStart)); + } + if (keypairExpiration != null && now.compareTo(keypairExpiration) >= 0) { + throw new InvalidParameterValueException(String.format("API key pair is expired, expiration date: %s", keypairExpiration)); + } + } + + public boolean hasEndDatePassed() { + Date now = DateTime.now().toDate(); + Date keypairExpiration = this.getEndDate(); + return keypairExpiration != null && now.compareTo(keypairExpiration) >= 0; + } + + public long getId() { + return id; + } + + public String getUuid() { + return uuid; + } + + public Long getUserId() { + return userId; + } + + public Date getStartDate() { + return startDate; + } + + public Date getEndDate() { + return endDate; + } + + public Date getCreated() { + return created; + } + + public String getDescription() { + return description; + } + + public String getApiKey() { + return apiKey; + } + + public String getSecretKey() { + return secretKey; + } + + public Class getEntityType() { + return ApiKeyPair.class; + } + + public String getName() { + return name; + } + + public Date getRemoved() { + return removed; + } + + @Override + public long getDomainId() { + return this.domainId; + } + + @Override + public long getAccountId() { + return this.accountId; + } + + public void setId(Long id) { this.id = id; } + + public void setUuid(String uuid) { + this.uuid = uuid; + } + + public void setUserId(Long userId) { + this.userId = userId; + } + + public void setStartDate(Date startDate) { + this.startDate = startDate; + } + + public void setEndDate(Date endDate) { + this.endDate = endDate; + } + + public void setDescription(String description) { + this.description = description; + } + + public void setApiKey(String apiKey) { + this.apiKey = apiKey; + } + + public void setSecretKey(String secretKey) { + this.secretKey = secretKey; + } + + public void setName(String name) { + this.name = name; + } + + public void setDomainId(Long domainId) { + this.domainId = domainId; + } + + public void setAccountId(Long accountId) { + this.accountId = accountId; + } + + public void setCreated(Date created) { + this.created = created; + } + + public void setRemoved(Date removed) { + this.removed = removed; + } +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/acl/RolePermissionBaseVO.java b/engine/schema/src/main/java/org/apache/cloudstack/acl/RolePermissionBaseVO.java index f3347ab6630..588fa029964 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/acl/RolePermissionBaseVO.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/acl/RolePermissionBaseVO.java @@ -50,6 +50,12 @@ public class RolePermissionBaseVO implements RolePermissionEntity { public RolePermissionBaseVO() { this.uuid = UUID.randomUUID().toString(); } + public RolePermissionBaseVO(final String rule, final Permission permission) { + this(); + this.rule = rule; + this.permission = permission; + } + public RolePermissionBaseVO(final String rule, final Permission permission, final String description) { this(); this.rule = rule; 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 00000000000..006c2afbc96 --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/acl/dao/ApiKeyPairDao.java @@ -0,0 +1,38 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.acl.dao; + +import com.cloud.utils.Pair; +import java.util.List; +import org.apache.cloudstack.acl.ApiKeyPairVO; +import com.cloud.utils.db.GenericDao; +import org.apache.cloudstack.api.command.admin.user.ListUserKeysCmd; + +public interface ApiKeyPairDao extends GenericDao { + ApiKeyPairVO findBySecretKey(String secretKey); + + ApiKeyPairVO findByApiKey(String apiKey); + + ApiKeyPairVO findByUuid(String uuid); + + Pair, Integer> listApiKeysByUserOrApiKeyId(Long userId, Long apiKeyId); + + ApiKeyPairVO getLastApiKeyCreatedByUser(Long userId); + + Pair, Integer> listByUserIdsPaginated(List userIds, ListUserKeysCmd cmd); + +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/acl/dao/ApiKeyPairDaoImpl.java b/engine/schema/src/main/java/org/apache/cloudstack/acl/dao/ApiKeyPairDaoImpl.java new file mode 100644 index 00000000000..a4895efed9e --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/acl/dao/ApiKeyPairDaoImpl.java @@ -0,0 +1,92 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.acl.dao; + +import com.cloud.utils.Pair; +import com.cloud.utils.db.Filter; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import java.util.List; +import org.apache.cloudstack.acl.ApiKeyPairVO; +import org.apache.cloudstack.api.command.admin.user.ListUserKeysCmd; +import org.apache.commons.collections.CollectionUtils; +import org.springframework.stereotype.Component; + +@Component +public class ApiKeyPairDaoImpl extends GenericDaoBase implements ApiKeyPairDao { + private static final String ID = "id"; + private static final String USER_ID = "userId"; + private static final String API_KEY = "apiKey"; + private static final String SECRET_KEY = "secretKey"; + + private final SearchBuilder keyPairSearch; + + ApiKeyPairDaoImpl() { + super(); + + keyPairSearch = createSearchBuilder(); + keyPairSearch.and(API_KEY, keyPairSearch.entity().getApiKey(), SearchCriteria.Op.EQ); + keyPairSearch.and(SECRET_KEY, keyPairSearch.entity().getSecretKey(), SearchCriteria.Op.EQ); + keyPairSearch.and(ID, keyPairSearch.entity().getId(), SearchCriteria.Op.EQ); + keyPairSearch.and(USER_ID, keyPairSearch.entity().getUserId(), SearchCriteria.Op.IN); + keyPairSearch.done(); + } + + @Override + public ApiKeyPairVO findByApiKey(String apiKey) { + SearchCriteria sc = keyPairSearch.create(); + sc.setParameters(API_KEY, apiKey); + return findOneBy(sc); + } + + public ApiKeyPairVO findBySecretKey(String secretKey) { + SearchCriteria sc = keyPairSearch.create(); + sc.setParameters(SECRET_KEY, secretKey); + return findOneBy(sc); + } + + public Pair, Integer> listApiKeysByUserOrApiKeyId(Long userId, Long apiKeyId) { + SearchCriteria sc = keyPairSearch.create(); + sc.setParametersIfNotNull(USER_ID, userId); + sc.setParametersIfNotNull(ID, apiKeyId); + final Filter searchFilter = new Filter(100); + return searchAndCount(sc, searchFilter); + } + + public ApiKeyPairVO getLastApiKeyCreatedByUser(Long userId) { + final SearchCriteria sc = keyPairSearch.create(); + sc.setParametersIfNotNull(USER_ID, userId); + final Filter searchBySorted = new Filter(ApiKeyPairVO.class, ID, false, null, null); + return findOneBy(sc, searchBySorted); + } + + 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(USER_ID, (Object[]) userIds.toArray(new Long[0])); + + Pair, Integer> apiKeyPairVOList = searchAndCount(sc, searchFilter); + if (CollectionUtils.isEmpty(apiKeyPairVOList.first())) { + return new Pair<>(List.of(), 0); + } + return apiKeyPairVOList; + } +} 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 00000000000..cbca2fd7274 --- /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 00000000000..4510e0ae289 --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/acl/dao/ApiKeyPairPermissionsDaoImpl.java @@ -0,0 +1,71 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.acl.dao; + +import com.cloud.utils.db.Filter; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import java.util.Collections; +import java.util.Objects; +import org.apache.commons.collections.CollectionUtils; +import org.apache.cloudstack.acl.ApiKeyPairPermissionVO; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +public class ApiKeyPairPermissionsDaoImpl extends GenericDaoBase implements ApiKeyPairPermissionsDao { + private static final String API_KEY_PAIR_ID = "apiKeyPairId"; + private static final String SORT_ORDER = "sortOrder"; + + private final SearchBuilder permissionByApiKeyPairIdSearch; + + public ApiKeyPairPermissionsDaoImpl() { + super(); + + permissionByApiKeyPairIdSearch = createSearchBuilder(); + permissionByApiKeyPairIdSearch.and(API_KEY_PAIR_ID, permissionByApiKeyPairIdSearch.entity().getApiKeyPairId(), SearchCriteria.Op.EQ); + permissionByApiKeyPairIdSearch.done(); + } + + public List findAllByApiKeyPairId(Long apiKeyPairId) { + SearchCriteria sc = permissionByApiKeyPairIdSearch.create(); + sc.setParameters(API_KEY_PAIR_ID, String.valueOf(apiKeyPairId)); + return listBy(sc); + } + + @Override + public ApiKeyPairPermissionVO persist(final ApiKeyPairPermissionVO item) { + item.setSortOrder(0); + final List permissionsList = findAllByKeyPairIdSorted(item.getApiKeyPairId()); + if (!CollectionUtils.isEmpty(permissionsList)) { + 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(API_KEY_PAIR_ID, apiKeyPairId); + final Filter searchBySorted = new Filter(ApiKeyPairPermissionVO.class, SORT_ORDER, true, null, null); + final List apiKeyPairPermissionList = listBy(sc, searchBySorted); + return Objects.requireNonNullElse(apiKeyPairPermissionList, Collections.emptyList()); + } +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/ImageStoreDetailsDaoImpl.java b/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/ImageStoreDetailsDaoImpl.java index ec40dc0dd68..d7e88bd31c3 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/ImageStoreDetailsDaoImpl.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/ImageStoreDetailsDaoImpl.java @@ -77,7 +77,7 @@ public class ImageStoreDetailsDaoImpl extends ResourceDetailsDaoBase + + diff --git a/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql b/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql index d330ecd0c0d..7923ef89ffd 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql @@ -49,3 +49,52 @@ CREATE TABLE IF NOT EXISTS `cloud`.`webhook_filter` ( INDEX `i_webhook_filter__webhook_id`(`webhook_id`), CONSTRAINT `fk_webhook_filter__webhook_id` FOREIGN KEY(`webhook_id`) REFERENCES `webhook`(`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- "api_keypair" table for API and secret keys +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`) +); + +-- "api_keypair_permissions" table for API key pairs permissions +CREATE TABLE IF NOT EXISTS `cloud`.`api_keypair_permissions` ( + `id` bigint(20) unsigned NOT NULL auto_increment, + `uuid` varchar(40) UNIQUE, + `sort_order` bigint(20) unsigned NOT NULL DEFAULT 0, + `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`) +); + +-- Populate "api_keypair" table with existing user API keys +INSERT INTO `cloud`.`api_keypair` (uuid, user_id, domain_id, account_id, api_key, secret_key, created, name) +SELECT UUID(), user.id, account.domain_id, account.id, user.api_key, user.secret_key, NOW(), 'Active key pair' +FROM `cloud`.`user` AS user +JOIN `cloud`.`account` AS account ON user.account_id = account.id +WHERE user.api_key IS NOT NULL AND user.secret_key IS NOT NULL; + +-- Drop API keys from user table +ALTER TABLE `cloud`.`user` DROP COLUMN api_key, DROP COLUMN secret_key; + +-- Grant access to the "deleteUserKeys" API to the "User", "Domain Admin" and "Resource Admin" roles, similarly to the "registerUserKeys" API +CALL `cloud`.`IDEMPOTENT_UPDATE_API_PERMISSION`('User', 'deleteUserKeys', 'ALLOW'); +CALL `cloud`.`IDEMPOTENT_UPDATE_API_PERMISSION`('Domain Admin', 'deleteUserKeys', 'ALLOW'); +CALL `cloud`.`IDEMPOTENT_UPDATE_API_PERMISSION`('Resource Admin', 'deleteUserKeys', 'ALLOW'); 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 340cfa9055f..dcba71e1098 100644 --- a/engine/schema/src/main/resources/META-INF/db/views/cloud.user_view.sql +++ b/engine/schema/src/main/resources/META-INF/db/views/cloud.user_view.sql @@ -29,8 +29,6 @@ select user.lastname, user.email, user.state, - user.api_key, - user.secret_key, user.created, user.removed, user.timezone, diff --git a/engine/storage/src/main/java/org/apache/cloudstack/storage/image/datastore/ImageStoreHelper.java b/engine/storage/src/main/java/org/apache/cloudstack/storage/image/datastore/ImageStoreHelper.java index dbb606b44a8..51edd62326d 100644 --- a/engine/storage/src/main/java/org/apache/cloudstack/storage/image/datastore/ImageStoreHelper.java +++ b/engine/storage/src/main/java/org/apache/cloudstack/storage/image/datastore/ImageStoreHelper.java @@ -131,7 +131,7 @@ public class ImageStoreHelper { String key = keyIter.next().toString(); String value = details.get(key); // encrypt swift key or s3 secret key - if (key.equals(ApiConstants.KEY) || key.equals(ApiConstants.S3_SECRET_KEY)) { + if (key.equals(ApiConstants.KEY) || key.equals(ApiConstants.SECRET_KEY)) { value = DBEncryptionUtil.encrypt(value); } ImageStoreDetailVO detail = new ImageStoreDetailVO(store.getId(), key, value, true); 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 030e0bcf014..f3e2335519a 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.acl.RolePermissionEntity.Permission; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.utils.cache.LazyCache; @@ -47,7 +50,7 @@ public class DynamicRoleBasedAPIAccessChecker extends AdapterBase implements API private RoleService roleService; private List services; - private Map> annotationRoleBasedApisMap = new HashMap>(); + private Map> annotationRoleBasedApisMap = new HashMap<>(); private LazyCache accountCache; private LazyCache>> rolePermissionsCache; @@ -56,7 +59,7 @@ public class DynamicRoleBasedAPIAccessChecker extends AdapterBase implements API protected DynamicRoleBasedAPIAccessChecker() { super(); for (RoleType roleType : RoleType.values()) { - annotationRoleBasedApisMap.put(roleType, new HashSet()); + annotationRoleBasedApisMap.put(roleType, new HashSet<>()); } } @@ -67,9 +70,12 @@ public class DynamicRoleBasedAPIAccessChecker extends AdapterBase implements API } List allPermissions = roleService.findAllPermissionsBy(role.getId()); + List allPermissionEntities = allPermissions.stream().map(permission -> (RolePermissionEntity) permission) + .collect(Collectors.toList()); + List allowedApis = new ArrayList<>(); for (String api : apiNames) { - if (checkApiPermissionByRole(role, api, allPermissions)) { + if (checkApiPermissionByRole(role, api, allPermissionEntities, false)) { allowedApis.add(api); } } @@ -84,8 +90,8 @@ public class DynamicRoleBasedAPIAccessChecker extends AdapterBase implements API * @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; } @@ -94,13 +100,13 @@ public class DynamicRoleBasedAPIAccessChecker extends AdapterBase implements API return false; } - if (logger.isTraceEnabled()) { - logger.trace(String.format("The API [%s] is allowed for the role %s by the permission [%s].", apiName, role, permission.getRule().toString())); - } + logger.trace("The API [{}] is allowed for the role {} by the permission [{}].", apiName, role, permission.getRule().toString()); return true; } + return annotationRoleBasedApisMap.get(role.getRoleType()) != null && - annotationRoleBasedApisMap.get(role.getRoleType()).contains(apiName); + annotationRoleBasedApisMap.get(role.getRoleType()).contains(apiName) && + !keyPairOverride; } protected Account getAccountFromId(long accountId) { @@ -135,49 +141,46 @@ public class DynamicRoleBasedAPIAccessChecker extends AdapterBase implements API } @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; } Account account = getAccountFromIdUsingCache(user.getAccountId()); if (account == null) { - throw new PermissionDeniedException(String.format("Account for user id [%s] cannot be found", user.getUuid())); + throw new PermissionDeniedException(String.format("Account for user with ID [%s] cannot be found", user.getUuid())); } - Pair> roleAndPermissions = getRolePermissionsUsingCache(account.getRoleId()); - final Role accountRole = roleAndPermissions.first(); - if (accountRole == null) { - throw new PermissionDeniedException(String.format("Account role for user id [%s] cannot be found.", user.getUuid())); - } - if (accountRole.getRoleType() == RoleType.Admin && accountRole.getId() == RoleType.Admin.getId()) { - logger.info("Account for user id {} is Root Admin or Domain Admin, all APIs are allowed.", user.getUuid()); - return true; - } - List allPermissions = roleAndPermissions.second(); - if (checkApiPermissionByRole(accountRole, commandName, allPermissions)) { - return true; - } - throw new UnavailableCommandException(String.format("The API [%s] does not exist or is not available for the account for user id [%s].", commandName, user.getUuid())); + + return checkAccess(account, commandName, apiKeyPairPermissions); } - public boolean checkAccess(Account account, String commandName) { + @Override + public boolean checkAccess(Account account, String commandName, ApiKeyPairPermission ... apiKeyPairPermissions) { Pair> roleAndPermissions = getRolePermissionsUsingCache(account.getRoleId()); final Role accountRole = roleAndPermissions.first(); if (accountRole == null) { throw new PermissionDeniedException(String.format("The account [%s] has role null or unknown.", account)); } - if (accountRole.getRoleType() == RoleType.Admin && accountRole.getId() == RoleType.Admin.getId()) { - if (logger.isTraceEnabled()) { - logger.trace(String.format("Account [%s] is Root Admin or Domain Admin, all APIs are allowed.", account)); - } + if (accountRole.getRoleType() == RoleType.Admin && accountRole.getId() == RoleType.Admin.getId() && apiKeyPairPermissions.length == 0) { + logger.info("Account [{}] is Root Admin and there aren't any API key pair permissions involved, thus, all APIs are allowed.", account); return true; } - List allPermissions = roleService.findAllPermissionsBy(accountRole.getId()); - if (checkApiPermissionByRole(accountRole, commandName, allPermissions)) { + boolean considerKeyPairPermissions = apiKeyPairPermissions.length > 0; + List allRules = considerKeyPairPermissions ? Arrays.asList(apiKeyPairPermissions) : new ArrayList<>(roleAndPermissions.second()); + if (checkApiPermissionByRole(accountRole, commandName, allRules, considerKeyPairPermissions)) { return true; } - throw new UnavailableCommandException(String.format("The API [%s] does not exist or is not available for the account %s.", commandName, account)); + + throw new UnavailableCommandException(String.format("The API [%s] does not exist or is not available for the account %s.", commandName, account.getAccountName())); + } + + @Override + public List getImplicitRolePermissions(RoleType roleType) { + return annotationRoleBasedApisMap.get(roleType) + .stream() + .map(implicitApi -> new RolePermissionBaseVO(implicitApi, Permission.ALLOW)) + .collect(Collectors.toList()); } /** 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 e58be3a75e7..71fbbb4a365 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.Arrays; import java.util.Collections; import java.util.List; +import com.cloud.exception.UnavailableCommandException; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPairPermission; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -195,4 +197,68 @@ public class DynamicRoleBasedAPIAccessCheckerTest extends TestCase { 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).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 2e7ae23d6f1..8513f458660 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.inject.Inject; 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 class ProjectRoleBasedApiAccessChecker extends AdapterBase implements AP } @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 class ProjectRoleBasedApiAccessChecker extends AdapterBase implements AP } @Override - public boolean checkAccess(Account account, String apiCommandName) throws PermissionDeniedException { + public boolean checkAccess(Account account, String apiCommandName, ApiKeyPairPermission... apiKeyPairPermissions) throws PermissionDeniedException { return true; } @@ -182,6 +183,11 @@ public class ProjectRoleBasedApiAccessChecker extends AdapterBase implements AP return true; } + @Override + public List getImplicitRolePermissions(RoleType roleType) { + return List.of(); + } + @Override public boolean start() { return super.start(); 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 3444f967d78..6cf4da88f5c 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 @@ -21,11 +21,13 @@ 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 com.cloud.exception.UnavailableCommandException; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPairPermission; import org.apache.cloudstack.api.APICommand; @@ -90,7 +92,7 @@ public class StaticRoleBasedAPIAccessChecker extends AdapterBase implements APIA } @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 +106,7 @@ public class StaticRoleBasedAPIAccessChecker extends AdapterBase implements APIA } @Override - public boolean checkAccess(Account account, String commandName) { + public boolean checkAccess(Account account, String commandName, ApiKeyPairPermission... apiKeyPairPermissions) { if (!isEnabled()) { return true; } @@ -163,6 +165,14 @@ public class StaticRoleBasedAPIAccessChecker extends AdapterBase implements APIA return super.start(); } + @Override + public List getImplicitRolePermissions(RoleType roleType) { + return annotationRoleBasedApisMap.get(roleType) + .stream() + .map(implicitApi -> new RolePermissionBaseVO(implicitApi, RolePermissionEntity.Permission.ALLOW)) + .collect(Collectors.toList()); + } + private void processMapping(Map configMap) { for (Map.Entry entry : configMap.entrySet()) { String apiName = entry.getKey(); diff --git a/plugins/api/discovery/pom.xml b/plugins/api/discovery/pom.xml index b0f29612911..1992da81e5f 100644 --- a/plugins/api/discovery/pom.xml +++ b/plugins/api/discovery/pom.xml @@ -39,6 +39,12 @@ cloud-utils ${project.version} + + org.apache.cloudstack + cloud-plugin-api-limit-account-based + ${project.version} + compile + diff --git a/plugins/api/discovery/src/main/java/org/apache/cloudstack/api/command/user/discovery/ListApisCmd.java b/plugins/api/discovery/src/main/java/org/apache/cloudstack/api/command/user/discovery/ListApisCmd.java index 9c01435fbf4..8cd31939da0 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 5a6eab7389d..073010a8eb6 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 @@ package org.apache.cloudstack.discovery; 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 @@ import java.util.List; public interface ApiDiscoveryService extends PluggableService { List listApiNames(Account account); - ListResponse listApis(User user, String apiName); + ListResponse listApis(User user, String apiName, ListApisCmd cmd); } diff --git a/plugins/api/discovery/src/main/java/org/apache/cloudstack/discovery/ApiDiscoveryServiceImpl.java b/plugins/api/discovery/src/main/java/org/apache/cloudstack/discovery/ApiDiscoveryServiceImpl.java index 4493f1e9074..d412f12fce2 100644 --- a/plugins/api/discovery/src/main/java/org/apache/cloudstack/discovery/ApiDiscoveryServiceImpl.java +++ b/plugins/api/discovery/src/main/java/org/apache/cloudstack/discovery/ApiDiscoveryServiceImpl.java @@ -26,6 +26,7 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; import javax.inject.Inject; @@ -33,6 +34,8 @@ import org.apache.cloudstack.acl.APIChecker; import org.apache.cloudstack.acl.Role; import org.apache.cloudstack.acl.RoleService; import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.acl.RolePermissionEntity; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPairService; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.BaseAsyncCmd; import org.apache.cloudstack.api.BaseAsyncCreateCmd; @@ -44,6 +47,7 @@ import org.apache.cloudstack.api.response.ApiDiscoveryResponse; 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.resourcedetail.UserDetailVO; import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; import org.apache.commons.collections.CollectionUtils; @@ -78,6 +82,9 @@ public class ApiDiscoveryServiceImpl extends ComponentLifecycleBase implements A @Inject RoleService roleService; + @Inject + ApiKeyPairService apiKeyPairService; + protected ApiDiscoveryServiceImpl() { super(); } @@ -257,16 +264,24 @@ public class ApiDiscoveryServiceImpl extends ComponentLifecycleBase implements A } @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) { + Account account = accountService.getAccount(user.getAccountId()); + 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, user, role, apisAllowed); + } else if (name != null) { if (!s_apiNameDiscoveryResponseMap.containsKey(name)) return null; @@ -281,11 +296,6 @@ public class ApiDiscoveryServiceImpl extends ComponentLifecycleBase implements A responseList.add(getApiDiscoveryResponseWithAccessibleParams(name, account)); } 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"))); @@ -343,6 +353,44 @@ public class ApiDiscoveryServiceImpl extends ComponentLifecycleBase implements A return cmdList; } + protected List listApisForKeyPair(String apiKey, String apiName, User user, Role role, List apisAllowed) { + List keyPairPermissions = accountService.getAllKeypairPermissions(apiKey); + + List filteredApis = new ArrayList<>(); + if (apiName != null && isApiAllowedForKey(keyPairPermissions, apiName)) { + filteredApis = List.of(apiName); + } else { + for (String api : apisAllowed) { + if (isApiAllowedForKey(keyPairPermissions, 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 d33774cad03..59415ccd1eb 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 @@ -99,7 +99,7 @@ public class ApiDiscoveryTest { @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) @@ -107,7 +107,7 @@ public class ApiDiscoveryTest { 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) @@ -117,7 +117,7 @@ public class ApiDiscoveryTest { 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 @@ -128,7 +128,7 @@ public class ApiDiscoveryTest { 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()); } @@ -140,7 +140,7 @@ public class ApiDiscoveryTest { 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()); } @@ -153,7 +153,7 @@ public class ApiDiscoveryTest { Mockito.when(mockUserAccount.getDetails()).thenReturn(userDetails); 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()); } @@ -166,7 +166,7 @@ public class ApiDiscoveryTest { Mockito.when(accountServiceMock.getAccount(Mockito.anyLong())).thenReturn(getNormalAccount()); Mockito.when(roleServiceMock.findRole(Mockito.anyLong())).thenReturn(userRoleVO); Mockito.when(apiNameDiscoveryResponseMapMock.get(Mockito.anyString())).thenReturn(Mockito.mock(ApiDiscoveryResponse.class)); - ListResponse response = (ListResponse) discoveryServiceSpy.listApis(getTestUser(), null); + ListResponse response = (ListResponse) discoveryServiceSpy.listApis(getTestUser(), null, null); Assert.assertEquals(4, response.getResponses().size()); } } 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 917cd7bb2b4..afa2b6155de 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,9 @@ import net.sf.ehcache.Cache; import net.sf.ehcache.CacheManager; import org.apache.cloudstack.acl.Role; +import org.apache.cloudstack.acl.RolePermissionEntity; +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPairPermission; import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; import org.springframework.stereotype.Component; @@ -161,17 +164,17 @@ public class ApiRateLimitServiceImpl extends AdapterBase implements APIChecker, } @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.", @@ -207,6 +210,11 @@ public class ApiRateLimitServiceImpl extends AdapterBase implements APIChecker, return true; } + @Override + public List getImplicitRolePermissions(RoleType roleType) { + return List.of(); + } + @Override public boolean isEnabled() { if (!enabled) { 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 42e94c1c017..f9456587058 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 @@ -252,7 +252,7 @@ public class QuotaResponseBuilderImpl implements QuotaResponseBuilder { } 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 1f5480404e4..3629bf2e3fe 100644 --- a/plugins/database/quota/src/test/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImplTest.java +++ b/plugins/database/quota/src/test/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImplTest.java @@ -652,7 +652,7 @@ public class QuotaResponseBuilderImplTest extends TestCase { ListResponse responseList = new ListResponse<>(); responseList.setResponses(cmdList); - Mockito.doReturn(responseList).when(discoveryServiceMock).listApis(userMock, null); + Mockito.doReturn(responseList).when(discoveryServiceMock).listApis(userMock, null, null); assertTrue(quotaResponseBuilderSpy.isUserAllowedToSeeActivationRules(userMock)); } @@ -668,7 +668,7 @@ public class QuotaResponseBuilderImplTest extends TestCase { ListResponse responseList = new ListResponse<>(); responseList.setResponses(cmdList); - Mockito.doReturn(responseList).when(discoveryServiceMock).listApis(userMock, null); + Mockito.doReturn(responseList).when(discoveryServiceMock).listApis(userMock, null, null); assertTrue(quotaResponseBuilderSpy.isUserAllowedToSeeActivationRules(userMock)); } @@ -684,7 +684,7 @@ public class QuotaResponseBuilderImplTest extends TestCase { ListResponse responseList = new ListResponse<>(); responseList.setResponses(cmdList); - Mockito.doReturn(responseList).when(discoveryServiceMock).listApis(userMock, null); + Mockito.doReturn(responseList).when(discoveryServiceMock).listApis(userMock, null, null); assertFalse(quotaResponseBuilderSpy.isUserAllowedToSeeActivationRules(userMock)); } diff --git a/plugins/hypervisors/baremetal/src/main/java/com/cloud/baremetal/manager/BaremetalVlanManagerImpl.java b/plugins/hypervisors/baremetal/src/main/java/com/cloud/baremetal/manager/BaremetalVlanManagerImpl.java index 5695325fb13..c05d52326ce 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 class BaremetalVlanManagerImpl extends ManagerBase implements BaremetalVl 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 d19470f8bab..f3ee6bd56b2 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 @@ -49,6 +49,7 @@ import javax.naming.ConfigurationException; import com.cloud.configuration.Resource; import com.cloud.user.ResourceLimitService; +import org.apache.cloudstack.acl.ApiKeyPairVO; import org.apache.cloudstack.acl.ControlledEntity; import org.apache.cloudstack.acl.Role; import org.apache.cloudstack.acl.RolePermissionEntity; @@ -1890,12 +1891,12 @@ public class KubernetesClusterManagerImpl extends ManagerBase implements Kuberne KUBEADMIN_ACCOUNT_NAME, "kubeadmin", null, UUID.randomUUID().toString(), User.Source.UNKNOWN)); keys = createUserApiKeyAndSecretKey(kube.getId()); } else { - String apiKey = kubeadmin.getApiKey(); - String secretKey = kubeadmin.getSecretKey(); - if (StringUtils.isAnyEmpty(apiKey, secretKey)) { + ApiKeyPairVO latestKeypair = ApiDBUtils.searchForLatestUserKeyPair(kubeadmin.getId()); + + if (latestKeypair == null) { keys = createUserApiKeyAndSecretKey(kubeadmin.getId()); } else { - keys = new String[]{apiKey, secretKey}; + keys = new String[]{latestKeypair.getApiKey(), latestKeypair.getSecretKey()}; } } return keys; diff --git a/plugins/network-elements/juniper-contrail/src/test/java/org/apache/cloudstack/network/contrail/management/MockAccountManager.java b/plugins/network-elements/juniper-contrail/src/test/java/org/apache/cloudstack/network/contrail/management/MockAccountManager.java index d3714f14834..7953a6a27cd 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,21 +25,30 @@ import javax.inject.Inject; import javax.naming.ConfigurationException; import com.cloud.api.auth.SetupUserTwoFactorAuthenticationCmd; +import org.apache.cloudstack.acl.RolePermissionEntity; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPairPermission; +import org.apache.cloudstack.api.BaseCmd; import org.apache.cloudstack.api.command.admin.account.CreateAccountCmd; +import org.apache.cloudstack.api.command.admin.user.DeleteUserCmd; +import org.apache.cloudstack.api.command.admin.user.DeleteUserKeysCmd; import org.apache.cloudstack.api.command.admin.user.GetUserKeysCmd; +import org.apache.cloudstack.api.command.admin.user.ListUserKeyRulesCmd; +import org.apache.cloudstack.api.command.admin.user.ListUserKeysCmd; import org.apache.cloudstack.api.command.admin.user.MoveUserCmd; +import org.apache.cloudstack.api.command.admin.user.RegisterUserKeysCmd; +import org.apache.cloudstack.api.command.admin.user.UpdateUserCmd; import org.apache.cloudstack.api.response.UserTwoFactorAuthenticationSetupResponse; import org.apache.cloudstack.auth.UserTwoFactorAuthenticator; import org.apache.cloudstack.backup.BackupOffering; import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPair; import org.apache.cloudstack.acl.ControlledEntity; import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.response.ApiKeyPairResponse; +import org.apache.cloudstack.api.response.ListResponse; import org.apache.cloudstack.acl.SecurityChecker.AccessType; import org.apache.cloudstack.api.command.admin.account.UpdateAccountCmd; -import org.apache.cloudstack.api.command.admin.user.DeleteUserCmd; -import org.apache.cloudstack.api.command.admin.user.RegisterUserKeyCmd; -import org.apache.cloudstack.api.command.admin.user.UpdateUserCmd; import org.apache.cloudstack.context.CallContext; import com.cloud.api.query.vo.ControlledViewEntity; @@ -119,7 +128,7 @@ public class MockAccountManager extends ManagerBase implements AccountManager { } @Override - public String[] createApiKeyAndSecretKey(RegisterUserKeyCmd arg0) { + public ApiKeyPair createApiKeyAndSecretKey(RegisterUserKeysCmd arg0) { // TODO Auto-generated method stub return null; } @@ -401,7 +410,7 @@ public class MockAccountManager extends ManagerBase implements AccountManager { } @Override - public Pair findUserByApiKey(String arg0) { + public Ternary findUserByApiKey(String arg0) { // TODO Auto-generated method stub return null; } @@ -466,6 +475,10 @@ public class MockAccountManager extends ManagerBase implements AccountManager { // TODO Auto-generated method stub } + @Override + public void validateCallingUserHasAccessToDesiredUser(Long userId) { + } + @Override public Long finalizeAccountId(String accountName, Long domainId, Long projectId, boolean enabledOnly) { // TODO Auto-generated method stub @@ -503,10 +516,23 @@ public class MockAccountManager extends ManagerBase implements AccountManager { } @Override - public Pair> getKeys(Long userId) { + public ListResponse listKeys(ListUserKeysCmd cmd) { return null; } + @Override + public List listKeyRules(ListUserKeyRulesCmd cmd) { + return null; + } + + @Override + public void deleteApiKey(DeleteUserKeysCmd cmd) { + } + + @Override + public void deleteApiKey(ApiKeyPair id) { + } + @Override public List listUserTwoFactorAuthenticationProviders() { return null; @@ -517,6 +543,31 @@ public class MockAccountManager extends ManagerBase implements AccountManager { return null; } + @Override + public ApiKeyPair getLatestUserKeyPair(Long userId) { + return null; + } + + @Override + public ApiKeyPair getKeyPairById(Long id) { + return null; + } + + @Override + public ApiKeyPair getKeyPairByApiKey(String apiKey) { + return null; + } + + @Override + public String getAccessingApiKey(BaseCmd cmd) { + return null; + } + + @Override + public List getAllKeypairPermissions(String apiKey) { + return List.of(); + } + @Override public void checkAccess(User user, ControlledEntity entity) throws PermissionDeniedException { @@ -538,7 +589,7 @@ public class MockAccountManager extends ManagerBase implements AccountManager { } @Override - public void checkApiAccess(Account account, String command) throws PermissionDeniedException { + public void checkApiAccess(Account account, String command, String apiKey) throws PermissionDeniedException { } @Override @@ -549,4 +600,12 @@ public class MockAccountManager extends ManagerBase implements AccountManager { @Override public void verifyCallerPrivilegeForUserOrAccountOperations(Account userAccount) { } + + @Override + public void verifyCallerPrivilegeForUserOrAccountOperations(User user) { + } + + @Override + public void checkCallerRoleTypeAllowedForUserOrAccountOperations(Account userAccount, User user) { + } } 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 60b5b7290a9..2a29b3976b0 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 @@ package org.apache.cloudstack.network.tungsten.service; import com.cloud.agent.AgentManager; import com.cloud.agent.api.Answer; import com.cloud.agent.api.Command; +import com.cloud.api.ApiDBUtils; import com.cloud.configuration.Config; import com.cloud.configuration.ConfigurationManager; import com.cloud.dc.DataCenter; @@ -114,6 +115,7 @@ import net.juniper.tungsten.api.types.TagType; import net.juniper.tungsten.api.types.VirtualMachine; import net.juniper.tungsten.api.types.VirtualMachineInterface; import net.juniper.tungsten.api.types.VirtualNetwork; +import org.apache.cloudstack.acl.ApiKeyPairVO; import org.apache.cloudstack.api.BaseResponse; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.framework.config.dao.ConfigurationDao; @@ -1182,9 +1184,10 @@ public class TungstenServiceImpl extends ManagerBase implements TungstenService int listenerPort = NetUtils.HTTPS_PORT; User callerUser = accountMgr.getActiveUser(CallContext.current().getCallingUserId()); - String apiKey = callerUser.getApiKey(); - String secretKey = callerUser.getSecretKey(); - if (apiKey != null && secretKey != null) { + ApiKeyPairVO latestKeypair = ApiDBUtils.searchForLatestUserKeyPair(callerUser.getId()); + if (latestKeypair != null) { + String apiKey = latestKeypair.getApiKey(); + String secretKey = latestKeypair.getSecretKey(); String url; try { String data = "apiKey=" + URLEncoder.encode(apiKey, StandardCharsets.UTF_8.name()).replace("\\+", "%20") + "&command" diff --git a/plugins/network-elements/tungsten/src/test/java/org/apache/cloudstack/network/tungsten/service/TungstenElementTest.java b/plugins/network-elements/tungsten/src/test/java/org/apache/cloudstack/network/tungsten/service/TungstenElementTest.java index b22d1e70be3..bad21464602 100644 --- a/plugins/network-elements/tungsten/src/test/java/org/apache/cloudstack/network/tungsten/service/TungstenElementTest.java +++ b/plugins/network-elements/tungsten/src/test/java/org/apache/cloudstack/network/tungsten/service/TungstenElementTest.java @@ -16,6 +16,7 @@ // under the License. package org.apache.cloudstack.network.tungsten.service; +import org.apache.cloudstack.acl.ApiKeyPairVO; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -360,6 +361,10 @@ public class TungstenElementTest { when(lbStickinessPolicy.getMethodName()).thenReturn("AppCookie"); List> pairList = List.of(new Pair<>("cookieName", "cookieValue")); + ApiKeyPairVO latest = new ApiKeyPairVO(); + latest.setApiKey("apikey"); + latest.setSecretKey("secretkey"); + when(ApiDBUtils.searchForLatestUserKeyPair(Mockito.anyLong())).thenReturn(latest); when(lbStickinessPolicy.getParams()).thenReturn(pairList); when(loadBalancingRule1.getId()).thenReturn(1L); when(loadBalancingRule1.getState()).thenReturn(FirewallRule.State.Add); diff --git a/plugins/storage/image/s3/src/main/java/org/apache/cloudstack/storage/datastore/driver/S3ImageStoreDriverImpl.java b/plugins/storage/image/s3/src/main/java/org/apache/cloudstack/storage/datastore/driver/S3ImageStoreDriverImpl.java index 9b2f3ddd100..6318c1920a9 100644 --- a/plugins/storage/image/s3/src/main/java/org/apache/cloudstack/storage/datastore/driver/S3ImageStoreDriverImpl.java +++ b/plugins/storage/image/s3/src/main/java/org/apache/cloudstack/storage/datastore/driver/S3ImageStoreDriverImpl.java @@ -55,7 +55,7 @@ public class S3ImageStoreDriverImpl extends BaseImageStoreDriverImpl { return new S3TO(imgStore.getId(), imgStore.getUuid(), details.get(ApiConstants.S3_ACCESS_KEY), - details.get(ApiConstants.S3_SECRET_KEY), + details.get(ApiConstants.SECRET_KEY), details.get(ApiConstants.S3_END_POINT), details.get(ApiConstants.S3_BUCKET_NAME), details.get(ApiConstants.S3_SIGNER), diff --git a/server/src/main/java/com/cloud/api/ApiDBUtils.java b/server/src/main/java/com/cloud/api/ApiDBUtils.java index 57eeb63ea9f..1d6d5d5c500 100644 --- a/server/src/main/java/com/cloud/api/ApiDBUtils.java +++ b/server/src/main/java/com/cloud/api/ApiDBUtils.java @@ -321,7 +321,6 @@ import com.cloud.template.TemplateManager; import com.cloud.template.VirtualMachineTemplate; import com.cloud.user.Account; import com.cloud.user.AccountDetailsDao; -import com.cloud.user.AccountManager; import com.cloud.user.AccountService; import com.cloud.user.AccountVO; import com.cloud.user.ResourceLimitService; @@ -361,6 +360,8 @@ import com.cloud.vm.dao.VMInstanceDetailsDao; import com.cloud.vm.dao.VMInstanceDao; import com.cloud.vm.snapshot.VMSnapshot; import com.cloud.vm.snapshot.dao.VMSnapshotDao; +import org.apache.cloudstack.acl.ApiKeyPairVO; +import org.apache.cloudstack.acl.dao.ApiKeyPairDao; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -498,6 +499,7 @@ public class ApiDBUtils { static BackupRepositoryDao s_backupRepositoryDao; static NicDao s_nicDao; static ResourceManagerUtil s_resourceManagerUtil; + static ApiKeyPairDao s_apiKeyPairDao; static SnapshotPolicyDetailsDao s_snapshotPolicyDetailsDao; static ObjectStoreDao s_objectStoreDao; @@ -764,6 +766,8 @@ public class ApiDBUtils { @Inject private ResourceManagerUtil resourceManagerUtil; @Inject + private ApiKeyPairDao apiKeyPairDao; + @Inject SnapshotPolicyDetailsDao snapshotPolicyDetailsDao; @Inject @@ -907,6 +911,7 @@ public class ApiDBUtils { s_backupRepositoryDao = backupRepositoryDao; s_resourceIconDao = resourceIconDao; s_resourceManagerUtil = resourceManagerUtil; + s_apiKeyPairDao = apiKeyPairDao; s_objectStoreDao = objectStoreDao; s_bucketDao = bucketDao; s_virtualMachineManager = virtualMachineManager; @@ -1984,10 +1989,8 @@ public class ApiDBUtils { } public static UserResponse newUserResponse(ResponseView view, Long domainId, UserAccountJoinVO usr) { - UserResponse response = s_userAccountJoinDao.newUserResponse(view, usr); - if(!AccountManager.UseSecretKeyInResponse.value()){ - response.setSecretKey(null); - } + ApiKeyPairVO lastKeyPair = searchForLatestUserKeyPair(usr.getId()); + UserResponse response = s_userAccountJoinDao.newUserResponse(view, usr, lastKeyPair); // Populate user account role information if (usr.getAccountRoleId() != null) { Role role = s_roleService.findRole( usr.getAccountRoleId()); @@ -2004,6 +2007,10 @@ public class ApiDBUtils { 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 655f5acb46e..51cf82b13b0 100644 --- a/server/src/main/java/com/cloud/api/ApiResponseHelper.java +++ b/server/src/main/java/com/cloud/api/ApiResponseHelper.java @@ -39,8 +39,16 @@ import java.util.stream.Collectors; import javax.inject.Inject; +import com.cloud.domain.dao.DomainDao; +import com.cloud.user.AccountVO; +import com.cloud.user.ApiKeyPairState; +import com.cloud.user.dao.AccountDao; import org.apache.cloudstack.acl.ControlledEntity; import org.apache.cloudstack.acl.ControlledEntity.ACLType; +import org.apache.cloudstack.acl.RoleVO; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPair; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPairPermission; +import org.apache.cloudstack.acl.dao.RoleDao; import org.apache.cloudstack.affinity.AffinityGroup; import org.apache.cloudstack.affinity.AffinityGroupResponse; import org.apache.cloudstack.annotation.AnnotationService; @@ -66,6 +74,7 @@ import org.apache.cloudstack.api.response.AutoScaleVmProfileResponse; import org.apache.cloudstack.api.response.BackupOfferingResponse; import org.apache.cloudstack.api.response.BackupRepositoryResponse; import org.apache.cloudstack.api.response.BackupScheduleResponse; +import org.apache.cloudstack.api.response.BaseRolePermissionResponse; import org.apache.cloudstack.api.response.BgpPeerResponse; import org.apache.cloudstack.api.response.BucketResponse; import org.apache.cloudstack.api.response.CapabilityResponse; @@ -113,6 +122,7 @@ import org.apache.cloudstack.api.response.IpRangeResponse; import org.apache.cloudstack.api.response.Ipv4RouteResponse; import org.apache.cloudstack.api.response.Ipv6RouteResponse; import org.apache.cloudstack.api.response.IsolationMethodResponse; +import org.apache.cloudstack.api.response.ApiKeyPairResponse; import org.apache.cloudstack.api.response.LBHealthCheckPolicyResponse; import org.apache.cloudstack.api.response.LBHealthCheckResponse; import org.apache.cloudstack.api.response.LBStickinessPolicyResponse; @@ -542,6 +552,15 @@ public class ApiResponseHelper implements ResponseGenerator { return domainPath.toString(); } + @Inject + private RoleDao roleDao; + + @Inject + private AccountDao accountDao; + + @Inject + private DomainDao domainDao; + @Override public UserResponse createUserResponse(User user) { UserAccountJoinVO vUser = ApiDBUtils.newUserView(user); @@ -5707,4 +5726,79 @@ protected Map getResourceIconsUsingOsCategory(List 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 8332578db49..dc07814c972 100644 --- a/server/src/main/java/com/cloud/api/ApiServer.java +++ b/server/src/main/java/com/cloud/api/ApiServer.java @@ -60,6 +60,7 @@ import javax.servlet.http.HttpSession; import com.cloud.cluster.ManagementServerHostVO; import com.cloud.cluster.dao.ManagementServerHostDao; +import com.cloud.utils.Ternary; import com.cloud.user.Account; import com.cloud.user.AccountManager; import com.cloud.user.AccountManagerImpl; @@ -68,6 +69,9 @@ import com.cloud.user.User; import com.cloud.user.UserAccount; import com.cloud.user.UserVO; import org.apache.cloudstack.acl.APIChecker; +import org.apache.cloudstack.acl.ApiKeyPairManagerImpl; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPair; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPairPermission; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.ApiErrorCode; @@ -237,6 +241,8 @@ public class ApiServer extends ManagerBase implements HttpRequestHandler, ApiSer private UUIDManager uuidMgr; @Inject private UserPasswordResetManager userPasswordResetManager; + @Inject + private ApiKeyPairManagerImpl keyPairManager; private List pluggableServices; @@ -1081,14 +1087,16 @@ public class ApiServer extends ManagerBase implements HttpRequestHandler, ApiSer txn.close(); User user; // verify there is a user with this api key - final Pair userAcctPair = accountMgr.findUserByApiKey(apiKey); - if (userAcctPair == null) { + final Ternary keyPairTernary = accountMgr.findUserByApiKey(apiKey); + + if (keyPairTernary == null) { logger.debug("apiKey does not map to a valid user -- ignoring request, apiKey: {}", apiKey); return false; } - user = userAcctPair.first(); - final Account account = userAcctPair.second(); + user = keyPairTernary.first(); + Account account = keyPairTernary.second(); + ApiKeyPair keyPair = keyPairTernary.third(); if (user.getState() != Account.State.ENABLED || !account.getState().equals(Account.State.ENABLED)) { logger.info("disabled or locked user accessing the api, user = {} (state: {}); " + @@ -1104,10 +1112,16 @@ public class ApiServer extends ManagerBase implements HttpRequestHandler, ApiSer return false; } - // verify secret key exists - secretKey = user.getSecretKey(); + if (keyPair.getRemoved() != null) { + logger.info(String.format("Invalid request, as used API keypair [%s] has been removed.", keyPair.getUuid())); + return false; + } + + keyPair.validateDate(); + + secretKey = keyPair.getSecretKey(); if (secretKey == null) { - logger.info("User does not have a secret key associated with the account -- ignoring request, username: {}", user); + logger.info(String.format("User does not have a secret key associated with the API key -- ignoring request, username: [%s].", user.getUsername())); return false; } @@ -1125,21 +1139,28 @@ public class ApiServer extends ManagerBase implements HttpRequestHandler, ApiSer if (!equalSig) { signature = signature.replaceAll(SANITIZATION_REGEX, "_"); logger.info("User signature [{}] is not equaled to computed signature [{}].", signature, computedSignature); - } else { - CallContext.register(user, account); + return false; + } + CallContext.register(user, account); + + List keyPairPermissions = keyPairManager.findAllPermissionsByKeyPairId(keyPair.getId(), account.getRoleId()); + if (commandAvailable(remoteAddress, commandName, user, keyPairPermissions.toArray(new ApiKeyPairPermission[0]))) { + logger.info("API accessed through API Key Pair. API Key: [{}].", keyPair.getApiKey()); + return true; } - return equalSig; } catch (final ServerApiException ex) { throw ex; + } catch (PermissionDeniedException ex) { + logger.error("Permission denied for keypair, reason: {}.", ex.getMessage()); } catch (final Exception ex) { - logger.error("unable to verify request signature"); + logger.error("Unable to verify request signature.", ex); } return false; } - private boolean commandAvailable(final InetAddress remoteAddress, final String commandName, final User user) { + private boolean commandAvailable(final InetAddress remoteAddress, final String commandName, final User user, ApiKeyPairPermission... rolePermissions) { try { - checkCommandAvailable(user, commandName, remoteAddress); + checkCommandAvailable(user, commandName, remoteAddress, rolePermissions); } catch (final RequestLimitException ex) { logger.debug(ex.getMessage()); throw new ServerApiException(ApiErrorCode.API_LIMIT_EXCEED, ex.getMessage()); @@ -1432,7 +1453,7 @@ public class ApiServer extends ManagerBase implements HttpRequestHandler, ApiSer return domainIdArr[0]; } - 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); } @@ -1446,12 +1467,11 @@ public class ApiServer extends ManagerBase implements HttpRequestHandler, ApiSer if (!NetUtils.isIpInCidrList(remoteAddress, accessAllowedCidrs.split(","))) { logger.warn("Request by account '{}' was denied since {} does not match {}", account.toString(), remoteAddress, accessAllowedCidrs); throw new OriginDeniedException("Calls from disallowed origin", account, remoteAddress); - } + } } - for (final APIChecker apiChecker : apiAccessCheckers) { - apiChecker.checkAccess(user, commandName); + apiChecker.checkAccess(user, commandName, apiKeyPairPermissions); } } diff --git a/server/src/main/java/com/cloud/api/ApiServlet.java b/server/src/main/java/com/cloud/api/ApiServlet.java index dd99b4e13ca..bfeb3d276e4 100644 --- a/server/src/main/java/com/cloud/api/ApiServlet.java +++ b/server/src/main/java/com/cloud/api/ApiServlet.java @@ -343,7 +343,7 @@ public class ApiServlet extends HttpServlet { return; } } else { - LOGGER.trace("no command available"); + LOGGER.trace("No command available."); } auditTrailSb.append(cleanQueryString); final boolean isNew = ((session == null) ? true : session.isNew()); @@ -353,7 +353,7 @@ public class ApiServlet extends HttpServlet { // if a API key exists if (isNew && LOGGER.isTraceEnabled()) { - LOGGER.trace(String.format("new session: %s", session)); + LOGGER.trace(String.format("New session: %s.", session)); } if (!isNew && (command.equalsIgnoreCase(ValidateUserTwoFactorAuthenticationCodeCmd.APINAME) || (!skip2FAcheckForAPIs(command) && !skip2FAcheckForUser(session)))) { diff --git a/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java b/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java index 0cec3a38075..fb97e2f3d8d 100644 --- a/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java +++ b/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java @@ -37,6 +37,13 @@ import java.util.stream.Stream; import javax.inject.Inject; +import com.cloud.network.PublicIpQuarantine; +import com.cloud.network.dao.PublicIpQuarantineDao; +import com.cloud.network.vo.PublicIpQuarantineVO; +import com.cloud.user.UserVO; +import org.apache.cloudstack.acl.RoleService; +import org.apache.cloudstack.acl.RoleVO; +import org.apache.cloudstack.acl.dao.RoleDao; import com.cloud.dc.Pod; import com.cloud.dc.dao.DataCenterDao; import com.cloud.dc.dao.HostPodDao; @@ -178,6 +185,7 @@ import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreVO; import org.apache.cloudstack.storage.datastore.db.StoragePoolDetailVO; import org.apache.cloudstack.storage.datastore.db.StoragePoolDetailsDao; import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; +import org.apache.cloudstack.utils.baremetal.BaremetalUtils; import org.apache.cloudstack.vm.lease.VMLeaseManager; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.collections.MapUtils; @@ -262,7 +270,6 @@ import com.cloud.host.dao.HostDao; import com.cloud.host.dao.HostTagsDao; import com.cloud.hypervisor.Hypervisor; import com.cloud.hypervisor.Hypervisor.HypervisorType; -import com.cloud.network.PublicIpQuarantine; import com.cloud.network.RouterHealthCheckResult; import com.cloud.network.VNF; import com.cloud.network.VpcVirtualNetworkApplianceService; @@ -272,13 +279,11 @@ import com.cloud.network.dao.IPAddressDao; import com.cloud.network.dao.IPAddressVO; import com.cloud.network.dao.NetworkDao; import com.cloud.network.dao.NetworkVO; -import com.cloud.network.dao.PublicIpQuarantineDao; import com.cloud.network.dao.RouterHealthCheckResultDao; import com.cloud.network.dao.RouterHealthCheckResultVO; import com.cloud.network.router.VirtualNetworkApplianceManager; import com.cloud.network.security.SecurityGroupVMMapVO; import com.cloud.network.security.dao.SecurityGroupVMMapDao; -import com.cloud.network.vo.PublicIpQuarantineVO; import com.cloud.offering.DiskOffering; import com.cloud.offering.ServiceOffering; import com.cloud.org.Grouping; @@ -377,6 +382,9 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q @Inject AccountManager accountMgr; + @Inject + RoleService roleService; + @Inject ProjectManager _projectMgr; @@ -646,6 +654,9 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q @Inject ExtensionHelper extensionHelper; + @Inject + RoleDao roleDao; + /* * (non-Javadoc) * @@ -825,6 +836,37 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q return _userAccountJoinDao.searchAndCount(sc, searchFilter); } + @Override + public List searchForAccessibleUsers() { + List permittedAccounts = new ArrayList<>(); + Account callingAccount = CallContext.current().getCallingAccount(); + Filter searchFilter = new Filter(UserAccountJoinVO.class, "id", true); + List allowedRoles = roleDao.listAll(); + roleService.removeRolesIfNeeded(allowedRoles); + List allowedRolesId = allowedRoles.stream().map(RoleVO::getId).collect(Collectors.toList()); + + Pair, Integer> usersPair = getUserListInternal(callingAccount, permittedAccounts, + true, null, null, null, null, null, null, null, callingAccount.getDomainId(), true, searchFilter, null); + return usersPair.first().stream().filter(userAccount -> { + if (BaremetalUtils.BAREMETAL_SYSTEM_ACCOUNT_NAME.equals(userAccount.getUsername()) && !accountMgr.isRootAdmin(callingAccount.getId())) { + return false; + } + + AccountVO accountVO = _accountDao.findByIdIncludingRemoved(userAccount.getAccountId()); + UserVO userVO = userDao.findByIdIncludingRemoved(userAccount.getId()); + if (ObjectUtils.anyNull(accountVO, userVO)) { + return false; + } + + try { + accountMgr.checkCallerRoleTypeAllowedForUserOrAccountOperations(accountVO, userVO); + } catch (PermissionDeniedException exception) { + return false; + } + return allowedRolesId.contains(userAccount.getAccountRoleId()); + }).map(UserAccountJoinVO::getId).collect(Collectors.toList()); + } + @Override public ListResponse searchForEvents(ListEventsCmd cmd) { Pair, Integer> result = searchForEventsInternal(cmd); diff --git a/server/src/main/java/com/cloud/api/query/dao/UserAccountJoinDao.java b/server/src/main/java/com/cloud/api/query/dao/UserAccountJoinDao.java index cff758d0c17..e0d88fbfde7 100644 --- a/server/src/main/java/com/cloud/api/query/dao/UserAccountJoinDao.java +++ b/server/src/main/java/com/cloud/api/query/dao/UserAccountJoinDao.java @@ -19,6 +19,7 @@ package com.cloud.api.query.dao; import java.util.List; import org.apache.cloudstack.api.ResponseObject; +import org.apache.cloudstack.acl.ApiKeyPairVO; import org.apache.cloudstack.api.response.UserResponse; import com.cloud.api.query.vo.UserAccountJoinVO; @@ -28,7 +29,7 @@ import com.cloud.utils.db.GenericDao; public interface UserAccountJoinDao extends GenericDao { - UserResponse newUserResponse(ResponseObject.ResponseView responseView, UserAccountJoinVO usr); + UserResponse newUserResponse(ResponseObject.ResponseView responseView, UserAccountJoinVO usr, ApiKeyPairVO lastKeyPair); UserAccountJoinVO newUserView(User usr); diff --git a/server/src/main/java/com/cloud/api/query/dao/UserAccountJoinDaoImpl.java b/server/src/main/java/com/cloud/api/query/dao/UserAccountJoinDaoImpl.java index f2c234b4c7c..f7dddb3ac07 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,8 +19,10 @@ package com.cloud.api.query.dao; import java.util.List; +import com.cloud.user.AccountManager; import com.cloud.user.AccountManagerImpl; import org.apache.cloudstack.api.ResponseObject.ResponseView; +import org.apache.cloudstack.acl.ApiKeyPairVO; import org.springframework.stereotype.Component; import org.apache.cloudstack.api.response.UserResponse; @@ -53,7 +55,7 @@ public class UserAccountJoinDaoImpl extends GenericDaoBase extends ManagerBase implements if (user == null) { throw new InvalidParameterValueException("Unable to find user by id " + autoscaleUserId); } - apiKey = user.getApiKey(); - secretKey = user.getSecretKey(); - if (apiKey == null) { - throw new InvalidParameterValueException("apiKey for user: " + user.getUsername() + " is empty. Please generate it"); + + ApiKeyPairVO latestKeypair = ApiDBUtils.searchForLatestUserKeyPair(user.getId()); + + if (latestKeypair == null) { + throw new InvalidParameterValueException(String.format("No API keypair for user [%s]. Please generate it.", user.getUsername())); } - if (secretKey == null) { - throw new InvalidParameterValueException("secretKey for user: " + user.getUsername() + " is empty. Please generate it"); - } + apiKey = latestKeypair.getApiKey(); + secretKey = latestKeypair.getSecretKey(); ApiServiceConfiguration.validateEndpointUrl(); } 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 e579eeeecd6..19a5896e425 100644 --- a/server/src/main/java/com/cloud/network/router/VirtualNetworkApplianceManagerImpl.java +++ b/server/src/main/java/com/cloud/network/router/VirtualNetworkApplianceManagerImpl.java @@ -17,6 +17,7 @@ package com.cloud.network.router; +import com.cloud.api.ApiDBUtils; import static com.cloud.utils.NumbersUtil.toHumanReadableSize; import static com.cloud.vm.VirtualMachineManager.SystemVmEnableUserData; @@ -51,6 +52,7 @@ import javax.naming.ConfigurationException; import com.google.gson.JsonSyntaxException; import com.google.gson.reflect.TypeToken; +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; @@ -2083,8 +2085,14 @@ Configurable, StateListener userAcctPair = _accountMgr.findUserByApiKey(apiKey); - if (userAcctPair == null) { + Ternary keyPairTernary = _accountMgr.findUserByApiKey(apiKey); + if (keyPairTernary == null) { LOGGER.debug("apiKey does not map to a valid user -- ignoring request, apiKey: " + apiKey); return false; } - user = userAcctPair.first(); - Account account = userAcctPair.second(); + user = keyPairTernary.first(); + Account account = keyPairTernary.second(); + ApiKeyPair keyPair = keyPairTernary.third(); if (!user.getState().equals(Account.State.ENABLED) || !account.getState().equals(Account.State.ENABLED)) { LOGGER.debug("disabled or locked user accessing the api, user: {}; state: {}; accountState: {}", user, user.getState(), account.getState()); return false; } - // verify secret key exists - secretKey = user.getSecretKey(); - if (secretKey == null) { - LOGGER.debug("User does not have a secret key associated with the account -- ignoring request, user: {}", user); + if (keyPair == null) { + LOGGER.debug("User does not have a keypair associated with the account -- ignoring request, username: {}", user.getUsername()); return false; } diff --git a/server/src/main/java/com/cloud/user/AccountManager.java b/server/src/main/java/com/cloud/user/AccountManager.java index c46ac78526b..98d2419e048 100644 --- a/server/src/main/java/com/cloud/user/AccountManager.java +++ b/server/src/main/java/com/cloud/user/AccountManager.java @@ -21,6 +21,7 @@ import java.util.List; import java.util.Map; import org.apache.cloudstack.acl.ControlledEntity; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPair; import org.apache.cloudstack.api.command.admin.account.UpdateAccountCmd; import org.apache.cloudstack.api.command.admin.user.DeleteUserCmd; import org.apache.cloudstack.api.command.admin.user.MoveUserCmd; @@ -35,7 +36,6 @@ import com.cloud.api.query.vo.ControlledViewEntity; import com.cloud.exception.ConcurrentOperationException; import com.cloud.exception.ResourceUnavailableException; import com.cloud.projects.Project.ListProjectResourcesCriteria; -import com.cloud.utils.Pair; import com.cloud.utils.Ternary; import com.cloud.utils.db.SearchBuilder; import com.cloud.utils.db.SearchCriteria; @@ -85,7 +85,7 @@ public interface AccountManager extends AccountService, Configurable { * that was created for a particular user * @return the user/account pair if one exact match was found, null otherwise */ - Pair findUserByApiKey(String apiKey); + Ternary findUserByApiKey(String apiKey); boolean enableAccount(long accountId); @@ -202,9 +202,13 @@ public interface AccountManager extends AccountService, Configurable { void validateUserPasswordAndUpdateIfNeeded(String newPassword, UserVO user, String currentPassword, boolean skipCurrentPassValidation); - void checkApiAccess(Account caller, String command); + void checkApiAccess(Account caller, String command, String apiKey); UserAccount clearUserTwoFactorAuthenticationInSetupStateOnLogin(UserAccount user); void verifyCallerPrivilegeForUserOrAccountOperations(Account userAccount); + + void verifyCallerPrivilegeForUserOrAccountOperations(User user); + + void checkCallerRoleTypeAllowedForUserOrAccountOperations(Account userAccount, User user); } diff --git a/server/src/main/java/com/cloud/user/AccountManagerImpl.java b/server/src/main/java/com/cloud/user/AccountManagerImpl.java index 09ef9fe8bec..d9af7af6c33 100644 --- a/server/src/main/java/com/cloud/user/AccountManagerImpl.java +++ b/server/src/main/java/com/cloud/user/AccountManagerImpl.java @@ -25,6 +25,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; +import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashSet; @@ -44,27 +45,48 @@ import javax.crypto.spec.SecretKeySpec; import javax.inject.Inject; import javax.naming.ConfigurationException; +import com.cloud.user.dao.AccountDao; +import com.cloud.user.dao.SSHKeyPairDao; +import com.cloud.user.dao.UserAccountDao; +import com.cloud.user.dao.UserDao; import org.apache.cloudstack.acl.APIChecker; +import org.apache.cloudstack.acl.ApiKeyPairManagerImpl; +import org.apache.cloudstack.acl.ApiKeyPairPermissionVO; +import org.apache.cloudstack.acl.ApiKeyPairVO; import org.apache.cloudstack.acl.ControlledEntity; import org.apache.cloudstack.acl.InfrastructureEntity; import org.apache.cloudstack.acl.QuerySelector; import org.apache.cloudstack.acl.Role; +import org.apache.cloudstack.acl.RolePermission; +import org.apache.cloudstack.acl.RolePermissionEntity; import org.apache.cloudstack.acl.RoleService; import org.apache.cloudstack.acl.RoleType; import org.apache.cloudstack.acl.SecurityChecker; import org.apache.cloudstack.acl.SecurityChecker.AccessType; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPair; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPairPermission; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPairService; +import org.apache.cloudstack.acl.dao.ApiKeyPairDao; +import org.apache.cloudstack.acl.dao.ApiKeyPairPermissionsDao; import org.apache.cloudstack.affinity.AffinityGroup; import org.apache.cloudstack.affinity.dao.AffinityGroupDao; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiCommandResourceType; import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseAsyncCmd; import org.apache.cloudstack.api.command.admin.account.CreateAccountCmd; import org.apache.cloudstack.api.command.admin.account.UpdateAccountCmd; import org.apache.cloudstack.api.command.admin.user.DeleteUserCmd; +import org.apache.cloudstack.api.command.admin.user.DeleteUserKeysCmd; import org.apache.cloudstack.api.command.admin.user.GetUserKeysCmd; +import org.apache.cloudstack.api.command.admin.user.ListUserKeyRulesCmd; +import org.apache.cloudstack.api.command.admin.user.ListUserKeysCmd; import org.apache.cloudstack.api.command.admin.user.MoveUserCmd; -import org.apache.cloudstack.api.command.admin.user.RegisterUserKeyCmd; +import org.apache.cloudstack.api.command.admin.user.RegisterUserKeysCmd; import org.apache.cloudstack.api.command.admin.user.UpdateUserCmd; +import org.apache.cloudstack.api.response.ApiKeyPairResponse; +import org.apache.cloudstack.api.response.BaseRolePermissionResponse; +import org.apache.cloudstack.api.response.ListResponse; import org.apache.cloudstack.api.response.UserTwoFactorAuthenticationSetupResponse; import org.apache.cloudstack.auth.UserAuthenticator; import org.apache.cloudstack.auth.UserAuthenticator.ActionOnFailedAuthentication; @@ -78,6 +100,7 @@ import org.apache.cloudstack.framework.config.dao.ConfigurationDao; import org.apache.cloudstack.framework.messagebus.MessageBus; import org.apache.cloudstack.framework.messagebus.PublishScope; import org.apache.cloudstack.managed.context.ManagedContextRunnable; +import org.apache.cloudstack.query.QueryService; import org.apache.cloudstack.network.RoutedIpv4Manager; import org.apache.cloudstack.network.dao.NetworkPermissionDao; import org.apache.cloudstack.region.gslb.GlobalLoadBalancerRuleDao; @@ -88,6 +111,8 @@ import org.apache.cloudstack.webhook.WebhookHelper; import org.apache.commons.codec.binary.Base64; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang3.BooleanUtils; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; import org.jetbrains.annotations.NotNull; import org.springframework.beans.factory.NoSuchBeanDefinitionException; @@ -170,17 +195,12 @@ import com.cloud.storage.snapshot.SnapshotManager; import com.cloud.template.TemplateManager; import com.cloud.template.VirtualMachineTemplate; import com.cloud.user.Account.State; -import com.cloud.user.dao.AccountDao; -import com.cloud.user.dao.SSHKeyPairDao; -import com.cloud.user.dao.UserAccountDao; -import com.cloud.user.dao.UserDao; import com.cloud.user.dao.UserDataDao; import com.cloud.utils.ConstantTimeComparator; import com.cloud.utils.NumbersUtil; import com.cloud.utils.Pair; import com.cloud.utils.Ternary; import com.cloud.utils.UuidUtils; -import com.cloud.utils.StringUtils; import com.cloud.utils.component.ComponentContext; import com.cloud.utils.component.Manager; import com.cloud.utils.component.ManagerBase; @@ -212,6 +232,8 @@ import com.cloud.vm.snapshot.VMSnapshot; import com.cloud.vm.snapshot.VMSnapshotManager; import com.cloud.vm.snapshot.VMSnapshotVO; import com.cloud.vm.snapshot.dao.VMSnapshotDao; +import org.apache.cloudstack.api.BaseCmd; +import org.joda.time.DateTime; public class AccountManagerImpl extends ManagerBase implements AccountManager, Manager { @@ -228,6 +250,14 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M @Inject private InstanceGroupDao _vmGroupDao; @Inject + private ApiKeyPairDao apiKeyPairDao; + @Inject + private ApiKeyPairService apiKeyPairService; + @Inject + private ApiKeyPairPermissionsDao apiKeyPairPermissionsDao; + @Inject + private ApiKeyPairManagerImpl keyPairManager; + @Inject private UserAccountDao userAccountDao; @Inject private VolumeDao _volumeDao; @@ -294,6 +324,8 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M @Inject private VolumeApiService volumeService; @Inject + private QueryService queryService; + @Inject private AffinityGroupDao _affinityGroupDao; @Inject private AccountGuestVlanMapDao _accountGuestVlanMapDao; @@ -398,6 +430,8 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M true, ConfigKey.Scope.Domain); + private Map> annotationRoleBasedApisMap = new HashMap<>(); + static ConfigKey userAllowMultipleAccounts = new ConfigKey<>("Advanced", Boolean.class, "user.allow.multiple.accounts", @@ -424,6 +458,9 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M protected AccountManagerImpl() { super(); + for (RoleType roleType : RoleType.values()) { + annotationRoleBasedApisMap.put(roleType, new HashSet<>()); + } } public List getUserAuthenticators() { @@ -903,6 +940,11 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M return cleanupAccount(account, callerUserId, caller); } + protected void removeUserApiKeys(Long userId) { + List apiKeyPairs = apiKeyPairDao.listApiKeysByUserOrApiKeyId(userId, null).first(); + apiKeyPairs.forEach(keyPair -> _accountService.deleteApiKey(keyPair)); + } + protected void cleanupPluginsResourcesIfNeeded(Account account) { try { KubernetesServiceHelper kubernetesServiceHelper = @@ -921,6 +963,7 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M // cleanup the users from the account List users = _userDao.listByAccount(accountId); for (UserVO user : users) { + removeUserApiKeys(user.getId()); if (!_userDao.remove(user.getId())) { logger.error("Unable to delete user: " + user + " as a part of account " + account + " cleanup"); accountCleanupNeeded = true; @@ -1469,16 +1512,25 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M } } - private void checkApiAccess(List apiCheckers, Account caller, String command) { + private void checkApiAccess(List apiCheckers, Account caller, String command, ApiKeyPairPermission... apiKeyPairPermissions) { for (final APIChecker apiChecker : apiCheckers) { - apiChecker.checkAccess(caller, command); + apiChecker.checkAccess(caller, command, apiKeyPairPermissions); } } @Override - public void checkApiAccess(Account caller, String command) { + public void checkApiAccess(Account caller, String command, String apiKey) { List apiCheckers = getEnabledApiCheckers(); - checkApiAccess(apiCheckers, caller, command); + + List keyPairPermissions = new ArrayList<>(); + if (apiKey != null) { + Ternary keyPairTernary = findUserByApiKey(apiKey); + if (keyPairTernary != null) { + keyPairPermissions = keyPairManager.findAllPermissionsByKeyPairId(keyPairTernary.third().getId(), caller.getRoleId()); + } + } + + checkApiAccess(apiCheckers, caller, command, keyPairPermissions.toArray(new ApiKeyPairPermission[0])); } @NotNull @@ -1641,8 +1693,9 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M } } - protected void verifyCallerPrivilegeForUserOrAccountOperations(User user) { - logger.debug(String.format("Verifying whether the caller has the correct privileges based on the user's role type and API permissions: %s", user)); + @Override + public void verifyCallerPrivilegeForUserOrAccountOperations(User user) { + logger.debug("Verifying whether the caller has the correct privileges based on the user's role type and API permissions: {}", user); Account userAccount = getAccount(user.getAccountId()); if (!Account.Type.PROJECT.equals(userAccount.getType())) { @@ -1651,7 +1704,8 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M } } - protected void checkCallerRoleTypeAllowedForUserOrAccountOperations(Account userAccount, User user) { + @Override + public void checkCallerRoleTypeAllowedForUserOrAccountOperations(Account userAccount, User user) { Account callingAccount = getCurrentCallingAccount(); RoleType callerRoleType = getRoleType(callingAccount); RoleType userAccountRoleType = getRoleType(userAccount); @@ -1892,7 +1946,7 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M } /** - * Validates user API and Secret keys. If a new pair of keys is provided, we update them in the user POJO. + * Validates user API and Secret keys. If a new pair of keys is provided, we update them in the key pair POJO. *
    *
  • When updating the keys, it must be provided a pair (API and Secret keys); otherwise, an {@link InvalidParameterValueException} is thrown. *
  • 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. @@ -1905,19 +1959,25 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M boolean isApiKeyBlank = StringUtils.isBlank(apiKey); boolean isSecretKeyBlank = StringUtils.isBlank(secretKey); if (isApiKeyBlank ^ isSecretKeyBlank) { - throw new InvalidParameterValueException("Please provide a userApiKey/userSecretKey pair"); + throw new InvalidParameterValueException("Please provide a valid API and secret key pair."); } if (isApiKeyBlank && isSecretKeyBlank) { return; } - UserAccount apiKeyOwner = userAccountDao.getUserByApiKey(apiKey); - if (apiKeyOwner != null) { - if (apiKeyOwner.getId() != user.getId()) { - throw new InvalidParameterValueException(String.format("The API key [%s] already exists in the system. Please provide a unique key.", apiKey)); - } + + ApiKeyPairVO lastUserKeyPair = apiKeyPairDao.getLastApiKeyCreatedByUser(user.getId()); + if (lastUserKeyPair == null) { + throw new InvalidParameterValueException(String.format("User [%s] has no active API key pairs to be updated.", user.getUsername())); } - user.setApiKey(apiKey); - user.setSecretKey(secretKey); + + Ternary keyPairTernary = findUserByApiKey(apiKey); + if (keyPairTernary != null) { + throw new InvalidParameterValueException(String.format("The API key [%s] already exists in the system. Please provide a unique key.", apiKey)); + } + + lastUserKeyPair.setApiKey(apiKey); + lastUserKeyPair.setSecretKey(secretKey); + apiKeyPairDao.update(lastUserKeyPair.getId(), lastUserKeyPair); } protected void validateAndUpdateUserApiKeyAccess(UpdateUserCmd updateUserCmd, UserVO user) { @@ -2418,6 +2478,9 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M // don't allow to delete the user from the account of type Project checkAccountAndAccess(user, account); verifyCallerPrivilegeForUserOrAccountOperations(user); + + removeUserApiKeys(id); + return _userDao.remove(id); } @@ -2457,8 +2520,6 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M 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()); @@ -3094,37 +3155,46 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M } @Override - public Pair findUserByApiKey(String apiKey) { - UserAccount userAccount = userAccountDao.getUserByApiKey(apiKey); - if (userAccount != null) { - User user = _userDao.getUser(userAccount.getId()); - Account account = _accountDao.findById(userAccount.getAccountId()); - return new Pair<>(user, account); - } else { + public Ternary findUserByApiKey(String apiKey) { + ApiKeyPairVO keyPairVO = apiKeyPairDao.findByApiKey(apiKey); + if (keyPairVO == null) { return null; } + + User user = _userDao.getUser(keyPairVO.getUserId()); + Account account = _accountDao.findById(keyPairVO.getAccountId()); + return new Ternary<>(user, account, keyPairVO); } @Override public Pair> getKeys(GetUserKeysCmd cmd) { - final long userId = cmd.getID(); - return getKeys(userId); - } - - @Override - public Pair> getKeys(Long userId) { + final long userId = cmd.getId(); User user = getActiveUser(userId); if (user == null) { - throw new InvalidParameterValueException("Unable to find user by id"); + throw new InvalidParameterValueException(String.format("Unable to find active user with ID [%s].", userId)); } - final Account account = getAccount(getUserAccountById(userId).getAccountId()); //Extracting the Account from the userID of the requested user. + final Account account = getAccount(user.getAccountId()); User caller = CallContext.current().getCallingUser(); checkAccess(caller, account); verifyCallerPrivilegeForUserOrAccountOperations(user); + String accessingApiKey = getAccessingApiKey(cmd); + ApiKeyPair keyPair; + if (accessingApiKey != null) { + ApiKeyPair accessingKeyPair = apiKeyPairService.findByApiKey(accessingApiKey); + if (userId == accessingKeyPair.getUserId()) { + keyPair = apiKeyPairService.findByApiKey(accessingApiKey); + } else { + keyPair = _accountService.getLatestUserKeyPair(userId); + } + } else { + keyPair = _accountService.getLatestUserKeyPair(userId); + } + Map keys = new HashMap<>(); - keys.put("apikey", user.getApiKey()); - keys.put("secretkey", user.getSecretKey()); + boolean isAllowed = keyPair != null && isAccessingKeypairSuperset(keyPair, cmd); + keys.put("apikey", isAllowed ? keyPair.getApiKey() : null); + keys.put("secretkey", isAllowed ? keyPair.getSecretKey() : null); Boolean apiKeyAccess = user.getApiKeyAccess(); if (apiKeyAccess == null) { @@ -3137,6 +3207,206 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M return new Pair<>(apiKeyAccess, keys); } + @Override + public ListResponse listKeys(ListUserKeysCmd cmd) { + ListResponse finalResponse = new ListResponse<>(); + List responses = new ArrayList<>(); + + if (cmd.getKeyId() != null || cmd.getApiKeyFilter() != null) { + fetchOnlyOneKeyPair(responses, cmd); + finalResponse.setResponses(responses); + return finalResponse; + } + + Integer total = fetchMultipleKeyPairs(responses, cmd); + finalResponse.setResponses(responses, total); + return finalResponse; + } + + private void fetchOnlyOneKeyPair(List responses, ListUserKeysCmd cmd) { + ApiKeyPair keyPair; + if (cmd.getKeyId() != null) { + keyPair = _accountService.getKeyPairById(cmd.getKeyId()); + } else { + keyPair = _accountService.getKeyPairByApiKey(cmd.getApiKeyFilter()); + } + + validateKeyPairIsNotNull(keyPair); + validateAccessingKeyPairPermissionsIsSupersetOfAccessedKeyPair(keyPair, cmd); + validateAccessToApiKey(keyPair); + removeApiKeyPairIfExpired(keyPair); + + addKeypairResponse(keyPair, responses, cmd); + } + + private void validateAccessToApiKey(ApiKeyPair keyPair) { + Account caller = getCurrentCallingAccount(); + logger.debug("Verifying if caller [{}] has access to API key pair whose ID is [{}].", caller.getAccountName(), keyPair.getUuid()); + Account account = _accountDao.findById(keyPair.getAccountId()); + checkAccess(caller, null, false, account); + _accountService.validateCallingUserHasAccessToDesiredUser(keyPair.getUserId()); + } + + private Integer fetchMultipleKeyPairs(List responses, ListUserKeysCmd cmd) { + List users; + if (cmd.getUserId() != null) { + _accountService.validateCallingUserHasAccessToDesiredUser(cmd.getUserId()); + users = List.of(cmd.getUserId()); + } else { + User callerUser = CallContext.current().getCallingUser(); + users = cmd.getListAll() && isAdmin(callerUser.getAccountId()) ? queryService.searchForAccessibleUsers() : List.of(callerUser.getId()); + } + + Pair, Integer> keyPairs = apiKeyPairDao.listByUserIdsPaginated(users, cmd); + keyPairs.first().stream() + .filter(keyPair -> isAccessingKeypairSuperset(keyPair, cmd)) + .forEach(keyPair -> { + addKeypairResponse(keyPair, responses, cmd); + removeApiKeyPairIfExpired(keyPair); + }); + + return keyPairs.second(); + } + + @Override + public List listKeyRules(ListUserKeyRulesCmd cmd) { + ApiKeyPair keyPair = apiKeyPairService.findById(cmd.getId()); + + validateKeyPairIsNotNull(keyPair); + validateAccessingKeyPairPermissionsIsSupersetOfAccessedKeyPair(keyPair, cmd); + _accountService.validateCallingUserHasAccessToDesiredUser(keyPair.getUserId()); + + Account account = _accountDao.findById(keyPair.getAccountId()); + return apiKeyPairService.findAllPermissionsByKeyPairId(keyPair.getId(), account.getRoleId()); + } + + private void validateKeyPairIsNotNull(ApiKeyPair keyPair) { + if (keyPair == null) { + logger.info("Keypair not found."); + throw new InvalidParameterValueException("Could not complete request."); + } + } + + private void validateAccessingKeyPairPermissionsIsSupersetOfAccessedKeyPair(ApiKeyPair keyPair, BaseCmd cmd) { + if (!isAccessingKeypairSuperset(keyPair, cmd)) { + logger.info("Accessing API key pair [{}] has less permissions than accessed API key pair.", keyPair.getId()); + throw new PermissionDeniedException("Could not complete request."); + } + } + + private Boolean isAccessingKeypairSuperset(ApiKeyPair accessedKeyPair, BaseCmd cmd) { + String apiKey = getAccessingApiKey(cmd); + if (apiKey == null) { + return Boolean.TRUE; + } + ApiKeyPair accessingKeyPair = apiKeyPairService.findByApiKey(apiKey); + return isApiKeySupersetOfPermission(new ArrayList<>(getAllKeypairPermissions(accessingKeyPair.getApiKey())), new ArrayList<>(getAllKeypairPermissions(accessedKeyPair.getApiKey()))); + } + + @Override + public String getAccessingApiKey(BaseCmd cmd) { + try { + if (cmd instanceof BaseAsyncCmd && ((BaseAsyncCmd) cmd).getJob().toString().contains("\"signature\"")) { + return parseApiKeyFromAsyncJob((BaseAsyncCmd) cmd); + } + boolean accessedByApiKey = cmd.getFullUrlParams().containsKey(ApiConstants.SIGNATURE); + String accessingApiKey = cmd.getFullUrlParams().get("apiKey"); + if (accessedByApiKey) { + return accessingApiKey; + } + } catch (NullPointerException e) { + logger.info("Accessing API through session."); + } + return null; + } + + private String parseApiKeyFromAsyncJob(BaseAsyncCmd cmd) { + String jobString = cmd.getJob().toString(); + int indexOfApiKey = jobString.indexOf("apiKey") + 9; + return jobString.substring(indexOfApiKey, jobString.indexOf("\"", indexOfApiKey)); + } + + private Boolean isApiKeySupersetOfPermission(List baseKeyPairPermissions, List comparedPermissions) { + Map apiNameToBaseKeyPermissions = roleService.getRoleRulesAndPermissions(baseKeyPairPermissions); + + return roleService.roleHasPermission(apiNameToBaseKeyPermissions, comparedPermissions); + } + + private void removeApiKeyPairIfExpired(ApiKeyPair apiKeyPair) { + if (apiKeyPair.hasEndDatePassed()) { + internalDeleteApiKey(apiKeyPair); + } + } + + public void deleteApiKey(DeleteUserKeysCmd cmd) { + ApiKeyPair keyPair = apiKeyPairService.findById(cmd.getId()); + if (keyPair == null) { + throw new InvalidParameterValueException(String.format("No keypair found with the ID [%s].", cmd.getId())); + } + _accountService.validateCallingUserHasAccessToDesiredUser(keyPair.getUserId()); + + deleteApiKey(keyPair); + } + + @Override + public void validateCallingUserHasAccessToDesiredUser(Long userId) { + User desiredUser = _userDao.getUser(userId); + if (desiredUser == null) { + throw new CloudRuntimeException(String.format("Unable to find user with ID [%s].", userId)); + } + verifyCallerPrivilegeForUserOrAccountOperations(desiredUser); + } + + @Override + public void deleteApiKey(ApiKeyPair keyPair) { + User user = _userDao.findByIdIncludingRemoved(keyPair.getUserId()); + if (user == null) { + throw new InvalidParameterValueException("User associated with the API key pair 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 a system account and the global setting " + + "baremetal.provision.done.notification is enabled. Therefore, it is not possible to delete API key pairs. If you wish to delete " + + "the baremetal user/account or their API Key, please disable the baremetal.provision.done.notification configuration.", user.getUuid())); + } + internalDeleteApiKey(keyPair); + } + + private void internalDeleteApiKey(ApiKeyPair keyPair) { + List permissions = apiKeyPairPermissionsDao.findAllByApiKeyPairId(keyPair.getId()); + for (ApiKeyPairPermission permission : permissions) { + apiKeyPairPermissionsDao.remove(permission.getId()); + } + apiKeyPairDao.remove(keyPair.getId()); + } + + private void addKeypairResponse(ApiKeyPair keyPair, List responses, ListUserKeysCmd cmd) { + if (keyPair == null) { + return; + } + ApiKeyPairResponse response = cmd._responseGenerator.createKeyPairResponse(keyPair); + if (Boolean.TRUE.equals(cmd.getShowPermissions())) { + Account account = _accountDao.findById(keyPair.getAccountId()); + List apiKeyPairPermissions = apiKeyPairService.findAllPermissionsByKeyPairId(keyPair.getId(), account.getRoleId()); + response.setPermissions(apiKeyPairPermissions.stream().map(apiKeyPairPermission -> { + BaseRolePermissionResponse rolePermissionResponse = new BaseRolePermissionResponse(); + rolePermissionResponse.setRule(apiKeyPairPermission.getRule()); + rolePermissionResponse.setDescription(apiKeyPairPermission.getDescription()); + rolePermissionResponse.setRulePermission(apiKeyPairPermission.getPermission()); + + return rolePermissionResponse; + }).collect(Collectors.toList())); + } + response.setObjectName(ApiConstants.USER_API_KEY); + responses.add(response); + } + + @Override + public ApiKeyPair getKeyPairById(Long id) { + return apiKeyPairDao.findById(id); + } + protected void preventRootDomainAdminAccessToRootAdminKeys(User caller, ControlledEntity account) { if (isDomainAdminForRootDomain(caller) && isRootAdmin(account.getAccountId())) { String msg = String.format("Caller Username %s does not have access to root admin keys", caller.getUsername()); @@ -3150,6 +3420,11 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M return caller.getType() == Account.Type.DOMAIN_ADMIN && caller.getDomainId() == Domain.ROOT_DOMAIN; } + @Override + public ApiKeyPair getKeyPairByApiKey(String apiKey) { + return apiKeyPairDao.findByApiKey(apiKey); + } + @Override public List listUserTwoFactorAuthenticationProviders() { return userTwoFactorAuthenticationProviders; @@ -3174,39 +3449,43 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M @Override @DB @ActionEvent(eventType = EventTypes.EVENT_REGISTER_FOR_SECRET_API_KEY, eventDescription = "register for the developer API keys") - public String[] createApiKeyAndSecretKey(RegisterUserKeyCmd cmd) { + public ApiKeyPair createApiKeyAndSecretKey(RegisterUserKeysCmd cmd) { Account caller = getCurrentCallingAccount(); - final Long userId = cmd.getId(); - - User user = getUserIncludingRemoved(userId); + User user = _userDao.findById(cmd.getUserId()); if (user == null) { - throw new InvalidParameterValueException("unable to find user by id"); + throw new InvalidParameterValueException(String.format("Unable to find user by ID: %d", cmd.getUserId())); } + final String name = cmd.getName(); + final long userId = user.getId(); + final String description = cmd.getDescription(); + final Date startDate = cmd.getStartDate(); + final Date endDate = cmd.getEndDate(); + final List> rules = cmd.getRules(); + Account account = _accountDao.findById(user.getAccountId()); checkAccess(caller, null, true, account); verifyCallerPrivilegeForUserOrAccountOperations(user); - // don't allow updating system user - if (user.getId() == User.UID_SYSTEM) { - throw new PermissionDeniedException(String.format("user: %s is system account, update is not allowed", user)); - } - // don't allow baremetal system user - if (BaremetalUtils.BAREMETAL_SYSTEM_ACCOUNT_NAME.equals(user.getUsername())) { - throw new PermissionDeniedException(String.format("user: %s is system account, update is not allowed", user)); + if (BaremetalUtils.BAREMETAL_SYSTEM_ACCOUNT_NAME.equals(user.getUsername()) || user.getId() == User.UID_SYSTEM) { + throw new PermissionDeniedException(String.format("User ID: [%s] is a system account and, thus, the operation is not allowed.", user.getId())); } - // generate both an api key and a secret key, update the user table with the keys, return the keys to the user - final String[] keys = new String[2]; - Transaction.execute(new TransactionCallbackNoReturn() { - @Override - public void doInTransactionWithoutResult(TransactionStatus status) { - keys[0] = createUserApiKey(userId); - keys[1] = createUserSecretKey(userId); - } + 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 in the future."); + } + if (ObjectUtils.allNotNull(startDate, endDate) && startDate.compareTo(endDate) > -1) { + throw new InvalidParameterValueException("Please specify an end date that is after the start date."); + } + + final ApiKeyPairVO newApiKeyPair = new ApiKeyPairVO(name, userId, description, startDate, endDate, account); + return Transaction.execute((TransactionCallback) status -> { + createUserApiKey(userId, newApiKeyPair); + createUserSecretKey(userId, newApiKeyPair); + return validateAndPersistKeyPairAndPermissions(account, newApiKeyPair, rules, cmd); }); - - return keys; } @Override @@ -3221,37 +3500,91 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M Account account = _accountDao.findById(user.getAccountId()); checkAccess(caller, null, true, account); final String[] keys = new String[2]; + ApiKeyPairVO newTokenKeyPair = new ApiKeyPairVO(); + newTokenKeyPair.setName(String.valueOf(userId)); + newTokenKeyPair.setAccountId(user.getAccountId()); + newTokenKeyPair.setDomainId(account.getDomainId()); + newTokenKeyPair.setUserId(userId); + Transaction.execute(new TransactionCallbackNoReturn() { @Override public void doInTransactionWithoutResult(TransactionStatus status) { - keys[0] = AccountManagerImpl.this.createUserApiKey(userId); - keys[1] = AccountManagerImpl.this.createUserSecretKey(userId); + keys[0] = createUserApiKey(userId, newTokenKeyPair); + keys[1] = createUserSecretKey(userId, newTokenKeyPair); + apiKeyPairDao.persist(newTokenKeyPair); } }); return keys; } - private String createUserApiKey(long userId) { - try { - UserVO updatedUser = _userDao.createForUpdate(); + /** + * Persists the API key pair and its corresponding permissions. Verifies whether + * the key pair being created is a superset of its owner's permissions. + * @param account Account owner of the key pair. + * @param newApiKeyPair The key pair object to be persisted. + * @param rules The set of rules of the key pair. + * @param cmd The API's command. + * @return The persisted key pair object. + */ + @DB + private ApiKeyPairVO validateAndPersistKeyPairAndPermissions(Account account, ApiKeyPairVO newApiKeyPair, + List> rules, RegisterUserKeysCmd cmd) { + String accessingApiKey = getAccessingApiKey(cmd); + final Role accountRole = roleService.findRole(account.getRoleId()); + List allPermissions = accessingApiKey == null ? + roleService.findAllRolePermissionsEntityBy(accountRole.getId(), true) : getAllKeypairPermissions(accessingApiKey); + List 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 key pair being created has a bigger set of permissions than the account [%s] " + + "that owns it. This is not allowed.", account.getUuid())); + } + + ApiKeyPairVO savedApiKeyPair = apiKeyPairDao.persist(newApiKeyPair); + permissions.forEach(permission -> { + ApiKeyPairPermissionVO permissionVO = (ApiKeyPairPermissionVO) permission; + permissionVO.setApiKeyPairId(savedApiKeyPair.getId()); + apiKeyPairPermissionsDao.persist(permissionVO); + }); + return savedApiKeyPair; + } + + @Override + public List getAllKeypairPermissions(String apiKey) { + if (apiKey == null) { + throw new InvalidParameterValueException("API key not present in the request's URL and, thus, unable to fetch API key rules."); + } + ApiKeyPair apiKeyPair = keyPairManager.findByApiKey(apiKey); + Account account = _accountDao.findById(apiKeyPair.getAccountId()); + List keyPairPermissions = keyPairManager.findAllPermissionsByKeyPairId(apiKeyPair.getId(), account.getRoleId()); + return new ArrayList<>(keyPairPermissions); + } + + private String createUserApiKey(long userId, ApiKeyPairVO newApiKeyPair) { + try { String encodedKey; - UserAccount userAcct; + ApiKeyPair keyPair; int retryLimit = 10; do { // FIXME: what algorithm should we use for API keys? KeyGenerator generator = KeyGenerator.getInstance("HmacSHA1"); SecretKey key = generator.generateKey(); encodedKey = Base64.encodeBase64URLSafeString(key.getEncoded()); - userAcct = userAccountDao.getUserByApiKey(encodedKey); + keyPair = apiKeyPairDao.findByApiKey(encodedKey); retryLimit--; - } while ((userAcct != null) && (retryLimit >= 0)); + } while ((keyPair != null) && (retryLimit >= 0)); - if (userAcct != null) { + if (keyPair != null) { return null; } - updatedUser.setApiKey(encodedKey); - _userDao.update(userId, updatedUser); + newApiKeyPair.setApiKey(encodedKey); return encodedKey; } catch (NoSuchAlgorithmException ex) { logger.error("error generating secret key for user {}", userAccountDao.findById(userId), ex); @@ -3259,26 +3592,24 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M return null; } - private String createUserSecretKey(long userId) { + private String createUserSecretKey(long userId, ApiKeyPairVO newApiKeyPair) { try { - UserVO updatedUser = _userDao.createForUpdate(); String encodedKey; int retryLimit = 10; - UserVO userBySecretKey; + ApiKeyPairVO keyPairVO; do { KeyGenerator generator = KeyGenerator.getInstance("HmacSHA1"); SecretKey key = generator.generateKey(); encodedKey = Base64.encodeBase64URLSafeString(key.getEncoded()); - userBySecretKey = _userDao.findUserBySecretKey(encodedKey); + keyPairVO = apiKeyPairDao.findBySecretKey(encodedKey); retryLimit--; - } while ((userBySecretKey != null) && (retryLimit >= 0)); + } while ((keyPairVO != null) && (retryLimit >= 0)); - if (userBySecretKey != null) { + if (keyPairVO != null) { return null; } - updatedUser.setSecretKey(encodedKey); - _userDao.update(userId, updatedUser); + newApiKeyPair.setSecretKey(encodedKey); return encodedKey; } catch (NoSuchAlgorithmException ex) { logger.error("error generating secret key for user {}", userAccountDao.findById(userId), ex); @@ -3286,6 +3617,10 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M return null; } + public ApiKeyPair getLatestUserKeyPair(Long userId) { + return ApiDBUtils.searchForLatestUserKeyPair(userId); + } + @Override public void buildACLSearchBuilder(SearchBuilder sb, Long domainId, boolean isRecursive, List permittedAccounts, ListProjectResourcesCriteria listProjectResourcesCriteria) { @@ -3492,7 +3827,8 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M @Override public UserAccount getUserByApiKey(String apiKey) { - return userAccountDao.getUserByApiKey(apiKey); + ApiKeyPairVO keyPair = apiKeyPairDao.findByApiKey(apiKey); + return userAccountDao.findById(keyPair.getUserId()); } @Override @@ -3553,9 +3889,10 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M @Override public UserAccount getUserAccountById(Long userId) { UserAccount userAccount = userAccountDao.findById(userId); - Map details = _userDetailsDao.listDetailsKeyPairs(userId); - userAccount.setDetails(details); - + if (userAccount != null) { + Map details = _userDetailsDao.listDetailsKeyPairs(userId); + userAccount.setDetails(details); + } return userAccount; } diff --git a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java index 36f1f7a2f12..4597ef81965 100644 --- a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java +++ b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java @@ -60,6 +60,7 @@ import javax.naming.ConfigurationException; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.ParserConfigurationException; +import com.cloud.serializer.GsonHelper; import com.cloud.storage.SnapshotPolicyVO; import com.cloud.storage.dao.SnapshotPolicyDao; import org.apache.cloudstack.acl.ControlledEntity; @@ -132,6 +133,7 @@ import org.apache.cloudstack.framework.async.AsyncCallFuture; import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.cloudstack.framework.config.Configurable; import org.apache.cloudstack.framework.config.dao.ConfigurationDao; +import org.apache.cloudstack.framework.jobs.impl.AsyncJobVO; import org.apache.cloudstack.framework.messagebus.MessageBus; import org.apache.cloudstack.framework.messagebus.PublishScope; import org.apache.cloudstack.managed.context.ManagedContextRunnable; @@ -651,6 +653,8 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir protected static long ROOT_DEVICE_ID = 0; + private final Type jobParamsType = new TypeToken>() {}.getType(); + public List getKubernetesServiceHelpers() { return kubernetesServiceHelpers; } @@ -3459,14 +3463,14 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir return AllowUserExpungeRecoverVm.valueIn(accountId); } - protected void checkExpungeVmPermission (Account callingAccount) { + protected void checkExpungeVmPermission(Account callingAccount, String apiKey) { logger.debug(String.format("Checking if [%s] has permission for expunging VMs.", callingAccount)); if (!_accountMgr.isAdmin(callingAccount.getId()) && !getConfigAllowUserExpungeRecoverVm(callingAccount.getId())) { logger.error(String.format("Parameter [%s] can only be passed by Admin accounts or when the allow.user.expunge.recover.vm key is true.", ApiConstants.EXPUNGE)); throw new PermissionDeniedException("Account does not have permission for expunging."); } try { - _accountMgr.checkApiAccess(callingAccount, BaseCmd.getCommandNameByClass(ExpungeVMCmd.class)); + _accountMgr.checkApiAccess(callingAccount, BaseCmd.getCommandNameByClass(ExpungeVMCmd.class), apiKey); } catch (PermissionDeniedException ex) { logger.error(String.format("Role [%s] of [%s] does not have permission for expunging VMs.", callingAccount.getRoleId(), callingAccount)); throw new PermissionDeniedException("Account does not have permission for expunging."); @@ -3491,7 +3495,10 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir boolean expunge = cmd.getExpunge(); if (expunge) { - checkExpungeVmPermission(ctx.getCallingAccount()); + String jobParamsString = ((AsyncJobVO) cmd.getJob()).getCmdInfo(); + HashMap jobParams = GsonHelper.getGson().fromJson(jobParamsString, jobParamsType); + String apiKey = jobParams.get("apiKey"); + checkExpungeVmPermission(ctx.getCallingAccount(), apiKey); } // check if VM exists diff --git a/server/src/main/java/org/apache/cloudstack/acl/ApiKeyPairManagerImpl.java b/server/src/main/java/org/apache/cloudstack/acl/ApiKeyPairManagerImpl.java new file mode 100644 index 00000000000..07ad3835167 --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/acl/ApiKeyPairManagerImpl.java @@ -0,0 +1,87 @@ +// 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.utils.component.ManagerBase; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPairService; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPair; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPairPermission; +import org.apache.cloudstack.acl.dao.ApiKeyPairDao; +import org.apache.cloudstack.acl.dao.ApiKeyPairPermissionsDao; +import org.apache.commons.collections.CollectionUtils; + +import javax.inject.Inject; +import java.util.List; + +public class ApiKeyPairManagerImpl extends ManagerBase implements ApiKeyPairService { + @Inject + private ApiKeyPairDao apiKeyPairDao; + @Inject + private ApiKeyPairPermissionsDao apiKeyPairPermissionsDao; + @Inject + private RoleService roleService; + + @Override + public List findAllPermissionsByKeyPairId(Long apiKeyPairId, Long roleId) { + List keyPairPermissions = apiKeyPairPermissionsDao.findAllByKeyPairIdSorted(apiKeyPairId); + List rolePermissions = roleService.findAllRolePermissionsEntityBy(roleId, true); + + if (CollectionUtils.isEmpty(keyPairPermissions)) { + return rolePermissions.stream() + .map(rolePermission -> new ApiKeyPairPermissionVO(rolePermission.getRule().getRuleString(), rolePermission.getPermission(), rolePermission.getDescription())) + .collect(Collectors.toList()); + } + + Map rolePermissionInfo = roleService.getRoleRulesAndPermissions(rolePermissions); + if (roleService.roleHasPermission(rolePermissionInfo, new ArrayList<>(keyPairPermissions))) { + return new ArrayList<>(keyPairPermissions); + } + + Map keyPairPermissionInfo = roleService.getRoleRulesAndPermissions(new ArrayList<>(keyPairPermissions)); + return getRulesToBeKeptForTheKeyPair(rolePermissionInfo, keyPairPermissionInfo) + .entrySet().stream().map((permission) -> new ApiKeyPairPermissionVO(permission.getKey(), permission.getValue().getPermission(), permission.getValue().getDescription())) + .collect(Collectors.toList()); + } + + private Map getRulesToBeKeptForTheKeyPair(Map rolePermissions, Map keyPairPermissions) { + Map rulesToBeKept = new HashMap<>(); + for (Map.Entry keyPairPermission : keyPairPermissions.entrySet()) { + String rule = keyPairPermission.getKey(); + RolePermissionEntity permission = keyPairPermission.getValue(); + boolean permissionGrantedByRole = rolePermissions.containsKey(rule) && rolePermissions.get(rule).getPermission() == RolePermissionEntity.Permission.ALLOW; + if (permission.getPermission() == RolePermissionEntity.Permission.ALLOW && permissionGrantedByRole) { + rulesToBeKept.put(rule, permission); + } + } + return rulesToBeKept; + } + + @Override + public ApiKeyPair findByApiKey(String apiKey) { + return apiKeyPairDao.findByApiKey(apiKey); + } + + @Override + public ApiKeyPair findById(Long id) { + return apiKeyPairDao.findById(id); + } +} 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 d1ae1b44a51..0cf874d9a7f 100644 --- a/server/src/main/java/org/apache/cloudstack/acl/RoleManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/acl/RoleManagerImpl.java @@ -75,6 +75,7 @@ public class RoleManagerImpl extends ManagerBase implements RoleService, Configu private RolePermissionsDao rolePermissionsDao; @Inject private AccountManager accountManager; + private List apiAccessCheckers; public void checkCallerAccess() { if (!isEnabled()) { @@ -449,20 +450,21 @@ public class RoleManagerImpl extends ManagerBase implements RoleService, Configu /** * Removes roles from the given list if the role has different or more permissions than the user's calling the method role */ - protected int removeRolesIfNeeded(List roles) { + @Override + public int removeRolesIfNeeded(List roles) { if (roles.isEmpty()) { return 0; } Long callerRoleId = getCurrentAccount().getRoleId(); - Map callerRolePermissions = getRoleRulesAndPermissions(callerRoleId); + Map callerRolePermissions = getRoleRulesAndPermissions(findAllRolePermissionsEntityBy(callerRoleId, false)); 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(), false))) { continue; } @@ -473,17 +475,11 @@ public class RoleManagerImpl extends ManagerBase implements RoleService, Configu return count; } - /** - * Checks if the role of the caller account has compatible permissions of the specified role. - * For each permission of the role of the caller, the target role needs to contain the same permission. - * - * @param sourceRolePermissions the permissions of the caller role. - * @param targetRole the role that the caller role wants to access. - * @return True if the role can be accessed with the given permissions; false otherwise. - */ - protected boolean roleHasPermission(Map sourceRolePermissions, Role targetRole) { + + @Override + public boolean roleHasPermission(Map rolePermissions, List rolePermissionsToAccess) { Set rulesAlreadyCompared = new HashSet<>(); - for (RolePermission rolePermission : findAllPermissionsBy(targetRole.getId())) { + for (RolePermissionEntity rolePermission : rolePermissionsToAccess) { boolean permissionIsRegex = rolePermission.getRule().getRuleString().contains("*"); for (String apiName : accountManager.getApiNameList()) { @@ -491,7 +487,7 @@ public class RoleManagerImpl extends ManagerBase implements RoleService, Configu continue; } - if (rolePermission.getPermission() == Permission.ALLOW && (!sourceRolePermissions.containsKey(apiName) || sourceRolePermissions.get(apiName) == Permission.DENY)) { + if (rolePermission.getPermission() == Permission.ALLOW && (!rolePermissions.containsKey(apiName) || rolePermissions.get(apiName).getPermission() == Permission.DENY)) { return false; } @@ -506,34 +502,34 @@ public class RoleManagerImpl extends ManagerBase implements RoleService, Configu return true; } - /** - * Given a role ID, returns a {@link Map} containing the API name as the key and the {@link Permission} for the API as the value. - * - * @param roleId ID from role. - */ - public Map getRoleRulesAndPermissions(Long roleId) { - Map roleRulesAndPermissions = new HashMap<>(); + @Override + public Map getRoleRulesAndPermissions(List rolePermissions) { + Map roleRulesAndPermissions = new HashMap<>(); - for (RolePermission rolePermission : findAllPermissionsBy(roleId)) { + for (RolePermissionEntity rolePermission : rolePermissions) { boolean permissionIsRegex = rolePermission.getRule().getRuleString().contains("*"); - for (String apiName : accountManager.getApiNameList()) { - if (!rolePermission.getRule().matches(apiName)) { - continue; - } - - if (!roleRulesAndPermissions.containsKey(apiName)) { - roleRulesAndPermissions.put(apiName, rolePermission.getPermission()); - } - - if (!permissionIsRegex) { - break; - } - } + mapRolePermissionToApiNames(rolePermission, roleRulesAndPermissions, permissionIsRegex); } return roleRulesAndPermissions; } + private void mapRolePermissionToApiNames(RolePermissionEntity rolePermission, Map roleRulesAndPermissions, boolean permissionIsRegex) { + for (String apiName : accountManager.getApiNameList()) { + if (!rolePermission.getRule().matches(apiName)) { + continue; + } + + if (!roleRulesAndPermissions.containsKey(apiName)) { + roleRulesAndPermissions.put(apiName, rolePermission); + } + + if (!permissionIsRegex) { + break; + } + } + } + @Override public List findRolesByType(RoleType roleType) { return findRolesByType(roleType, null, null, null).first(); @@ -571,6 +567,43 @@ public class RoleManagerImpl extends ManagerBase implements RoleService, Configu return Collections.emptyList(); } + @Override + public List findAllRolePermissionsEntityBy(final Long roleId, final boolean considerImplicitRules) { + List rolePermissions = rolePermissionsDao.findAllByRoleIdSorted(roleId); + if (!considerImplicitRules) { + return new ArrayList<>(rolePermissions); + } + + List permissions = new ArrayList<>(); + List implicitPermissions = getImplicitRolePermissions(roleId); + for (RolePermissionEntity implicitPermission : implicitPermissions) { + boolean implicitPermissionAlreadyDefinedByExplicitPermissions = rolePermissions.stream() + .anyMatch(permission -> permission.getRule().matches(implicitPermission.getRule().getRuleString())); + if (!implicitPermissionAlreadyDefinedByExplicitPermissions) { + permissions.add(implicitPermission); + } + } + + permissions.addAll(rolePermissions); + return permissions; + } + + private List getImplicitRolePermissions(Long roleId) { + Role role = roleDao.findById(roleId); + if (role == null) { + return List.of(); + } + + for (APIChecker apiChecker : apiAccessCheckers) { + List implicitPermissions = apiChecker.getImplicitRolePermissions(role.getRoleType()); + if (apiChecker.isEnabled() && !CollectionUtils.isEmpty(implicitPermissions)) { + return implicitPermissions; + } + } + + return List.of(); + } + private boolean isCallerRootAdmin() { return accountManager.isRootAdmin(getCurrentAccount().getId()); } @@ -613,4 +646,9 @@ public class RoleManagerImpl extends ManagerBase implements RoleService, Configu cmdList.add(DisableRoleCmd.class); return cmdList; } + + @Inject + public void setApiAccessCheckers(List apiAccessCheckers) { + this.apiAccessCheckers = apiAccessCheckers; + } } 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 b90c40dc95e..37d32c0f390 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 @@ -37,10 +37,14 @@ value="#{pluggableAPIAuthenticatorsRegistry.registered}" /> - + + + + + 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 c186083b8ce..1b864bd695f 100644 --- a/server/src/test/java/com/cloud/network/as/AutoScaleManagerImplTest.java +++ b/server/src/test/java/com/cloud/network/as/AutoScaleManagerImplTest.java @@ -76,6 +76,9 @@ import org.mockito.Spy; import org.mockito.junit.MockitoJUnitRunner; import org.springframework.test.util.ReflectionTestUtils; +import com.cloud.api.ApiDBUtils; +import org.apache.cloudstack.acl.ApiKeyPairVO; +import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService; import com.cloud.agent.AgentManager; import com.cloud.agent.api.PerformanceMonitorAnswer; import com.cloud.agent.api.PerformanceMonitorCommand; @@ -266,9 +269,14 @@ public class AutoScaleManagerImplTest { @Mock GuestOSDao guestOSDao; + @Mock + NetworkOrchestrationService networkOrchestrationService; + AccountVO account; UserVO user; + MockedStatic mockedApiDBUtils; + final static String INVALID = "invalid"; private static final Long counterId = 1L; @@ -419,6 +427,11 @@ public class AutoScaleManagerImplTest { Mockito.doNothing().when(accountManager).checkAccess(Mockito.any(Account.class), Mockito.isNull(), Mockito.anyBoolean(), Mockito.any()); when(asPolicyDao.persist(any(AutoScalePolicyVO.class))).thenReturn(asScaleUpPolicyMock); + mockedApiDBUtils = Mockito.mockStatic(ApiDBUtils.class); + ApiKeyPairVO ret = new ApiKeyPairVO(); + ret.setSecretKey("secretkey"); + ret.setApiKey("apikey"); + when(ApiDBUtils.searchForLatestUserKeyPair(Mockito.any())).thenReturn(ret); userDataDetails.put("0", new HashMap<>() {{ put("key1", "value1"); put("key2", "value2"); }}); Mockito.doReturn(userDataFinal).when(userVmMgr).finalizeUserData(any(), any(), any()); @@ -432,6 +445,7 @@ public class AutoScaleManagerImplTest { @After public void tearDown() { + mockedApiDBUtils.close(); CallContext.unregister(); } @@ -879,9 +893,6 @@ public class AutoScaleManagerImplTest { public void testCheckAutoScaleUserSucceed() throws NoSuchFieldException, IllegalAccessException { when(userDao.findById(any())).thenReturn(userMock); when(userMock.getAccountId()).thenReturn(accountId); - when(userMock.getApiKey()).thenReturn(autoScaleUserApiKey); - when(userMock.getSecretKey()).thenReturn(autoScaleUserSecretKey); - final Field f = ConfigKey.class.getDeclaredField("_defaultValue"); f.setAccessible(true); f.set(ApiServiceConfiguration.ApiServletPath, "http://10.10.10.10:8080/client/api"); @@ -891,25 +902,6 @@ public class AutoScaleManagerImplTest { @Test(expected = InvalidParameterValueException.class) public void testCheckAutoScaleUserFail1() { - when(userDao.findById(any())).thenReturn(userMock); - when(userMock.getAccountId()).thenReturn(accountId); - when(userMock.getApiKey()).thenReturn(autoScaleUserApiKey); - when(userMock.getSecretKey()).thenReturn(null); - - autoScaleManagerImplSpy.checkAutoScaleUser(autoScaleUserId, accountId); - } - - @Test(expected = InvalidParameterValueException.class) - public void testCheckAutoScaleUserFail2() { - when(userDao.findById(any())).thenReturn(userMock); - when(userMock.getAccountId()).thenReturn(accountId); - when(userMock.getApiKey()).thenReturn(null); - - autoScaleManagerImplSpy.checkAutoScaleUser(autoScaleUserId, accountId); - } - - @Test(expected = InvalidParameterValueException.class) - public void testCheckAutoScaleUserFail3() { when(userDao.findById(any())).thenReturn(userMock); when(userMock.getAccountId()).thenReturn(accountId + 1L); @@ -917,18 +909,16 @@ public class AutoScaleManagerImplTest { } @Test(expected = InvalidParameterValueException.class) - public void testCheckAutoScaleUserFail4() { + public void testCheckAutoScaleUserFail2() { when(userDao.findById(any())).thenReturn(null); autoScaleManagerImplSpy.checkAutoScaleUser(autoScaleUserId, accountId); } @Test(expected = InvalidParameterValueException.class) - public void testCheckAutoScaleUserFail5() throws NoSuchFieldException, IllegalAccessException { + public void testCheckAutoScaleUserFail3() throws NoSuchFieldException, IllegalAccessException { when(userDao.findById(any())).thenReturn(userMock); when(userMock.getAccountId()).thenReturn(accountId); - when(userMock.getApiKey()).thenReturn(autoScaleUserApiKey); - when(userMock.getSecretKey()).thenReturn(autoScaleUserSecretKey); final Field f = ConfigKey.class.getDeclaredField("_defaultValue"); f.setAccessible(true); diff --git a/server/src/test/java/com/cloud/user/AccountManagerImplTest.java b/server/src/test/java/com/cloud/user/AccountManagerImplTest.java index e5d09ba9141..c476895ea50 100644 --- a/server/src/test/java/com/cloud/user/AccountManagerImplTest.java +++ b/server/src/test/java/com/cloud/user/AccountManagerImplTest.java @@ -19,12 +19,15 @@ package com.cloud.user; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.nullable; +import com.cloud.utils.Ternary; import java.net.InetAddress; import java.net.UnknownHostException; +import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; +import java.util.Date; import java.util.List; import java.util.Map; @@ -32,10 +35,24 @@ import org.apache.cloudstack.acl.ControlledEntity; import org.apache.cloudstack.acl.Role; import org.apache.cloudstack.acl.RoleService; import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.acl.ApiKeyPairPermissionVO; +import org.apache.cloudstack.acl.ApiKeyPairVO; +import org.apache.cloudstack.acl.RolePermission; +import org.apache.cloudstack.acl.RolePermissionEntity; +import org.apache.cloudstack.acl.RolePermissionVO; +import org.apache.cloudstack.acl.RoleVO; import org.apache.cloudstack.acl.SecurityChecker.AccessType; import org.apache.cloudstack.api.command.admin.account.UpdateAccountCmd; + +import org.apache.cloudstack.acl.apikeypair.ApiKeyPair; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPairService; +import org.apache.cloudstack.acl.dao.ApiKeyPairDao; +import org.apache.cloudstack.acl.dao.ApiKeyPairPermissionsDao; +import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.command.admin.user.DeleteUserCmd; import org.apache.cloudstack.api.command.admin.user.GetUserKeysCmd; +import org.apache.cloudstack.api.command.admin.user.ListUserKeysCmd; +import org.apache.cloudstack.api.command.admin.user.RegisterUserKeysCmd; import org.apache.cloudstack.api.command.admin.user.UpdateUserCmd; import org.apache.cloudstack.api.response.UserTwoFactorAuthenticationSetupResponse; import org.apache.cloudstack.auth.UserAuthenticator; @@ -86,7 +103,9 @@ public class AccountManagerImplTest extends AccountManagentImplTestBase { @Mock private AccountService accountService; @Mock - private GetUserKeysCmd _listkeyscmd; + private GetUserKeysCmd _getkeyscmd; + @Mock + private ListUserKeysCmd listUserKeysCmd; @Mock private User _user; @Mock @@ -102,6 +121,15 @@ public class AccountManagerImplTest extends AccountManagentImplTestBase { @Mock private UserVO userVoMock; + @Mock + private ApiKeyPairService apiKeyPairService; + + @Mock + private ApiKeyPairVO apiKeyPairVOMock; + + @Mock + private Pair, Integer> pairMock; + private final long accountMockId = 100L; @Mock @@ -128,9 +156,19 @@ public class AccountManagerImplTest extends AccountManagentImplTestBase { @Mock ConfigKey allowOperationsOnUsersInSameAccountMock; + @Mock RoleService roleService; + @Mock + ApiKeyPairPermissionsDao apiKeyPairPermissionsDaoMock; + + @Mock + ApiKeyPairDao apiKeyPairDaoMock; + + @Mock + RegisterUserKeysCmd registerCmdMock; + @Before public void setUp() throws Exception { enableUserTwoFactorAuthenticationMock = Mockito.mock(ConfigKey.class); @@ -150,6 +188,13 @@ public class AccountManagerImplTest extends AccountManagentImplTestBase { Mockito.doReturn(userVoIdMock).when(userVoMock).getId(); Mockito.lenient().doNothing().when(accountManagerImpl).checkRoleEscalation(accountMock, accountMock); + Mockito.doReturn(accountMockId).when(accountVoMock).getId(); + + Mockito.when(apiKeyPairDaoMock.persist(Mockito.any())).thenAnswer(i -> { + ApiKeyPairVO keyPair = (ApiKeyPairVO) i.getArguments()[0]; + keyPair.setId(1L); + return keyPair; + }); } @Test @@ -296,6 +341,7 @@ public class AccountManagerImplTest extends AccountManagentImplTestBase { Mockito.doNothing().when(accountManagerImpl).checkAccountAndAccess(Mockito.any(), Mockito.any()); Mockito.doNothing().when(accountManagerImpl).verifyCallerPrivilegeForUserOrAccountOperations(userVoMock); + Mockito.doNothing().when(accountManagerImpl).removeUserApiKeys(Mockito.anyLong()); accountManagerImpl.deleteUser(cmd); } } @@ -334,13 +380,10 @@ public class AccountManagerImplTest extends AccountManagentImplTestBase { } @Test(expected = PermissionDeniedException.class) - public void testgetUserCmd() { + public void testGetUserCmd() { CallContext.register(callingUser, callingAccount); // Calling account is user account i.e normal account - Mockito.when(_listkeyscmd.getID()).thenReturn(1L); + Mockito.when(_getkeyscmd.getId()).thenReturn(1L); Mockito.when(accountManagerImpl.getActiveUser(1L)).thenReturn(userVoMock); - Mockito.when(userAccountDao.findById(1L)).thenReturn(userAccountVO); - Mockito.when(userAccountVO.getAccountId()).thenReturn(1L); - Mockito.lenient().when(accountManagerImpl.getAccount(Mockito.anyLong())).thenReturn(accountMock); // Queried account - admin account Mockito.lenient().when(callingUser.getAccountId()).thenReturn(1L); Mockito.lenient().when(_accountDao.findById(1L)).thenReturn(callingAccount); @@ -348,17 +391,15 @@ public class AccountManagerImplTest extends AccountManagentImplTestBase { Mockito.lenient().when(accountService.isNormalUser(Mockito.anyLong())).thenReturn(Boolean.TRUE); Mockito.lenient().when(accountMock.getAccountId()).thenReturn(2L); - accountManagerImpl.getKeys(_listkeyscmd); + accountManagerImpl.getKeys(_getkeyscmd); } @Test(expected = PermissionDeniedException.class) public void testGetUserKeysCmdDomainAdminRootAdminUser() { CallContext.register(callingUser, callingAccount); - Mockito.when(_listkeyscmd.getID()).thenReturn(2L); + Mockito.when(_getkeyscmd.getId()).thenReturn(2L); Mockito.when(accountManagerImpl.getActiveUser(2L)).thenReturn(userVoMock); - Mockito.when(userAccountDao.findById(2L)).thenReturn(userAccountVO); - Mockito.when(userAccountVO.getAccountId()).thenReturn(2L); - Mockito.when(userDetailsDaoMock.listDetailsKeyPairs(Mockito.anyLong())).thenReturn(null); + Mockito.when(userVoMock.getAccountId()).thenReturn(2L); // Queried account - admin account AccountVO adminAccountMock = Mockito.mock(AccountVO.class); @@ -377,7 +418,7 @@ public class AccountManagerImplTest extends AccountManagentImplTestBase { Mockito.lenient().when(accountService.isDomainAdmin(Mockito.anyLong())).thenReturn(Boolean.TRUE); Mockito.lenient().when(accountMock.getAccountId()).thenReturn(2L); - accountManagerImpl.getKeys(_listkeyscmd); + accountManagerImpl.getKeys(_getkeyscmd); } @Test @@ -472,7 +513,6 @@ public class AccountManagerImplTest extends AccountManagentImplTestBase { 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(accountMock).when(accountManagerImpl).retrieveAndValidateAccount(userVoMock); Mockito.doNothing().when(accountManagerImpl).validateAndUpdateFirstNameIfNeeded(UpdateUserCmdMock, userVoMock); @@ -523,63 +563,73 @@ public class AccountManagerImplTest extends AccountManagentImplTestBase { } @Test - public void validateAndUpdatApiAndSecretKeyIfNeededTestNoKeys() { + public void validateAndUpdateApiAndSecretKeyIfNeededTestNoKeys() { accountManagerImpl.validateAndUpdateApiAndSecretKeyIfNeeded(UpdateUserCmdMock, userVoMock); - Mockito.verify(userAccountDao, Mockito.times(0)).getUserByApiKey(Mockito.anyString()); + Mockito.verify(apiKeyPairDaoMock, Mockito.times(0)).getLastApiKeyCreatedByUser(Mockito.anyLong()); + Mockito.verify(accountManagerImpl, Mockito.times(0)).findUserByApiKey(Mockito.anyString()); } @Test(expected = InvalidParameterValueException.class) - public void validateAndUpdatApiAndSecretKeyIfNeededTestOnlyApiKeyInformed() { + public void validateAndUpdateApiAndSecretKeyIfNeededTestOnlyApiKeyInformed() { Mockito.doReturn("apiKey").when(UpdateUserCmdMock).getApiKey(); accountManagerImpl.validateAndUpdateApiAndSecretKeyIfNeeded(UpdateUserCmdMock, userVoMock); } @Test(expected = InvalidParameterValueException.class) - public void validateAndUpdatApiAndSecretKeyIfNeededTestOnlySecretKeyInformed() { + public void validateAndUpdateApiAndSecretKeyIfNeededTestOnlySecretKeyInformed() { Mockito.doReturn("secretKey").when(UpdateUserCmdMock).getSecretKey(); accountManagerImpl.validateAndUpdateApiAndSecretKeyIfNeeded(UpdateUserCmdMock, userVoMock); } @Test(expected = InvalidParameterValueException.class) - public void validateAndUpdatApiAndSecretKeyIfNeededTestApiKeyAlreadyUsedBySomeoneElse() { + public void validateAndUpdateApiAndSecretKeyIfNeededTestApiKeyAlreadyUsedBySomeoneElse() { String apiKey = "apiKey"; Mockito.doReturn(apiKey).when(UpdateUserCmdMock).getApiKey(); Mockito.doReturn("secretKey").when(UpdateUserCmdMock).getSecretKey(); Mockito.doReturn(1L).when(userVoMock).getId(); + Mockito.doReturn(apiKeyPairVOMock).when(apiKeyPairDaoMock).getLastApiKeyCreatedByUser(1L); User otherUserMock = Mockito.mock(User.class); - UserAccount UserAccountMock = Mockito.mock(UserAccount.class); - Mockito.doReturn(UserAccountMock).when(userAccountDao).getUserByApiKey(apiKey); + Ternary pairUserAccountMock = new Ternary(otherUserMock, Mockito.mock(Account.class), apiKeyPairVOMock); + Mockito.doReturn(pairUserAccountMock).when(accountManagerImpl).findUserByApiKey(apiKey); accountManagerImpl.validateAndUpdateApiAndSecretKeyIfNeeded(UpdateUserCmdMock, userVoMock); } + @Test(expected = InvalidParameterValueException.class) + public void validateAndUpdateApiAndSecretKeyIfNeededTestUserHasNoActiveApiKeyPair() { + String apiKey = "apiKey"; + Mockito.doReturn(apiKey).when(UpdateUserCmdMock).getApiKey(); + Mockito.doReturn("secretKey").when(UpdateUserCmdMock).getSecretKey(); + Mockito.doReturn(1L).when(userVoMock).getId(); + accountManagerImpl.validateAndUpdateApiAndSecretKeyIfNeeded(UpdateUserCmdMock, userVoMock); + } + @Test - public void validateAndUpdatApiAndSecretKeyIfNeededTest() { + public void validateAndUpdateApiAndSecretKeyIfNeededTest() { String apiKey = "apiKey"; Mockito.doReturn(apiKey).when(UpdateUserCmdMock).getApiKey(); String secretKey = "secretKey"; Mockito.doReturn(secretKey).when(UpdateUserCmdMock).getSecretKey(); - User otherUserMock = Mockito.mock(User.class); - - Mockito.doReturn(null).when(userAccountDao).getUserByApiKey(apiKey); + Mockito.doReturn(1L).when(userVoMock).getId(); + Mockito.doReturn(apiKeyPairVOMock).when(apiKeyPairDaoMock).getLastApiKeyCreatedByUser(1L); accountManagerImpl.validateAndUpdateApiAndSecretKeyIfNeeded(UpdateUserCmdMock, userVoMock); - Mockito.verify(userAccountDao).getUserByApiKey(apiKey); - Mockito.verify(userVoMock).setApiKey(apiKey); - Mockito.verify(userVoMock).setSecretKey(secretKey); + Mockito.verify(accountManagerImpl).findUserByApiKey(apiKey); + Mockito.verify(apiKeyPairVOMock).setApiKey(apiKey); + Mockito.verify(apiKeyPairVOMock).setSecretKey(secretKey); } @Test - public void validateAndUpdatUserApiKeyAccess() { + public void validateAndUpdateUserApiKeyAccess() { Mockito.doReturn("Enabled").when(UpdateUserCmdMock).getApiKeyAccess(); try (MockedStatic eventUtils = Mockito.mockStatic(ActionEventUtils.class)) { Mockito.when(ActionEventUtils.onActionEvent(Mockito.anyLong(), Mockito.anyLong(), @@ -599,7 +649,7 @@ public class AccountManagerImplTest extends AccountManagentImplTestBase { } @Test - public void validateAndUpdatAccountApiKeyAccess() { + public void validateAndUpdateAccountApiKeyAccess() { Mockito.doReturn("Inherit").when(UpdateAccountCmdMock).getApiKeyAccess(); try (MockedStatic eventUtils = Mockito.mockStatic(ActionEventUtils.class)) { Mockito.when(ActionEventUtils.onActionEvent(Mockito.anyLong(), Mockito.anyLong(), @@ -613,7 +663,7 @@ public class AccountManagerImplTest extends AccountManagentImplTestBase { } @Test(expected = InvalidParameterValueException.class) - public void validateAndUpdatAccountApiKeyAccessInvalidParameter() { + public void validateAndUpdateAccountApiKeyAccessInvalidParameter() { Mockito.doReturn("False").when(UpdateAccountCmdMock).getApiKeyAccess(); accountManagerImpl.validateAndUpdateAccountApiKeyAccess(UpdateAccountCmdMock, accountVoMock); } @@ -1271,6 +1321,354 @@ public class AccountManagerImplTest extends AccountManagentImplTestBase { Assert.assertEquals(userAccountVOList.get(0), userAccounts.get(0)); } + @Test + public void createApiKeyAndSecretKeyTestWithEmptyRules() { + CallContext.register(callingUser, callingAccount); + Mockito.lenient().when(callingUser.getId()).thenReturn(111L); + long userId = 111L; + + Mockito.when(registerCmdMock.getRules()).thenReturn(List.of()); + Mockito.when(registerCmdMock.getUserId()).thenReturn(userId); + + Mockito.when(userDaoMock.findById(any())).thenReturn(userVoMock); + Mockito.when(_accountDao.findById(Mockito.anyLong())).thenReturn(accountVoMock); + Mockito.doNothing().when(accountManagerImpl).checkAccess(Mockito.any(Account.class), Mockito.isNull(), Mockito.anyBoolean(), Mockito.any(Account.class)); + Mockito.doNothing().when(accountManagerImpl).verifyCallerPrivilegeForUserOrAccountOperations(Mockito.any(User.class)); + Mockito.when(apiKeyPairDaoMock.findBySecretKey(Mockito.anyString())).thenReturn(null); + Mockito.when(roleService.findAllRolePermissionsEntityBy(Mockito.anyLong(), Mockito.anyBoolean())).thenReturn(List.of( + new RolePermissionVO(1L, "api2", RolePermissionEntity.Permission.ALLOW, "description") + )); + Mockito.when(roleService.findRole(Mockito.anyLong())).thenReturn(new RoleVO()); + Mockito.when(roleService.roleHasPermission(Mockito.anyMap(), Mockito.anyList())).thenReturn(true); + + accountManagerImpl.createApiKeyAndSecretKey(registerCmdMock); + Mockito.verify(apiKeyPairDaoMock, Mockito.times(0)).remove(Mockito.anyLong()); + Mockito.verify(apiKeyPairPermissionsDaoMock, Mockito.times(0)).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.doNothing().when(accountManagerImpl).verifyCallerPrivilegeForUserOrAccountOperations(Mockito.any(User.class)); + Mockito.when(roleService.findAllRolePermissionsEntityBy(Mockito.anyLong(), Mockito.anyBoolean())).thenReturn(List.of( + new RolePermissionVO(1L, "api2", RolePermissionEntity.Permission.ALLOW, "description") + )); + Mockito.when(roleService.findRole(Mockito.anyLong())).thenReturn(new RoleVO()); + + accountManagerImpl.createApiKeyAndSecretKey(registerCmdMock); + Mockito.verify(apiKeyPairDaoMock, Mockito.times(1)).remove(Mockito.anyLong()); + Mockito.verify(apiKeyPairPermissionsDaoMock, Mockito.times(0)).persist(Mockito.any(ApiKeyPairPermissionVO.class)); + } + + @Test(expected = InvalidParameterValueException.class) + public void createApiKeyAndSecretTestKeyDeniedOnAccount() { + CallContext.register(callingUser, callingAccount); + Mockito.lenient().when(callingUser.getId()).thenReturn(111L); + long userId = 111L; + + List> rules = new ArrayList<>(); + rules.add(Map.of( + ApiConstants.RULE, "api", + ApiConstants.PERMISSION, RolePermission.Permission.ALLOW, + ApiConstants.DESCRIPTION, "description" + )); + Mockito.when(registerCmdMock.getRules()).thenReturn(rules); + Mockito.when(registerCmdMock.getUserId()).thenReturn(userId); + + ApiKeyPairVO apiKeyPairVO = new ApiKeyPairVO(); + apiKeyPairVO.setUserId(userId); + apiKeyPairVO.setId(1L); + + Mockito.when(userDaoMock.findById(Mockito.anyLong())).thenReturn(userVoMock); + Mockito.when(_accountDao.findById(Mockito.anyLong())).thenReturn(accountVoMock); + Mockito.doNothing().when(accountManagerImpl).checkAccess(Mockito.any(Account.class), Mockito.isNull(), Mockito.anyBoolean(), Mockito.any()); + Mockito.doNothing().when(accountManagerImpl).verifyCallerPrivilegeForUserOrAccountOperations(Mockito.any(User.class)); + Mockito.when(apiKeyPairDaoMock.findBySecretKey(Mockito.anyString())).thenReturn(null); + Mockito.when(roleService.findAllRolePermissionsEntityBy(Mockito.anyLong(), Mockito.anyBoolean())).thenReturn(List.of( + new RolePermissionVO(1L, "api", RolePermissionEntity.Permission.DENY, "description") + )); + Mockito.when(roleService.findRole(Mockito.anyLong())).thenReturn(new RoleVO()); + + accountManagerImpl.createApiKeyAndSecretKey(registerCmdMock); + Mockito.verify(apiKeyPairDaoMock, Mockito.times(1)).remove(Mockito.anyLong()); + Mockito.verify(apiKeyPairPermissionsDaoMock, Mockito.times(0)).persist(Mockito.any(ApiKeyPairPermissionVO.class)); + } + + @Test + public void createApiKeyAndSecretKeyTestAllowedOnAccount() { + CallContext.register(callingUser, callingAccount); + Mockito.lenient().when(callingUser.getId()).thenReturn(111L); + long userId = callingUser.getId(); + + List> rules = new ArrayList<>(); + rules.add(Map.of( + ApiConstants.RULE, "api", + ApiConstants.PERMISSION, RolePermission.Permission.ALLOW, + ApiConstants.DESCRIPTION, "description" + )); + Mockito.when(registerCmdMock.getRules()).thenReturn(rules); + Mockito.when(registerCmdMock.getUserId()).thenReturn(userId); + + ApiKeyPairPermissionVO permissionVO = new ApiKeyPairPermissionVO(); + ApiKeyPairVO apiKeyPairVO = new ApiKeyPairVO(); + apiKeyPairVO.setUserId(userId); + apiKeyPairVO.setId(1L); + + Mockito.when(userDaoMock.findById(Mockito.anyLong())).thenReturn(userVoMock); + Mockito.when(_accountDao.findById(Mockito.anyLong())).thenReturn(accountVoMock); + Mockito.doNothing().when(accountManagerImpl).checkAccess(Mockito.any(Account.class), Mockito.isNull(), Mockito.anyBoolean(), Mockito.any(Account.class)); + Mockito.doNothing().when(accountManagerImpl).verifyCallerPrivilegeForUserOrAccountOperations(Mockito.any(User.class)); + Mockito.when(apiKeyPairPermissionsDaoMock.persist(Mockito.any(ApiKeyPairPermissionVO.class))).thenReturn(permissionVO); + Mockito.doReturn(true).when(roleService).roleHasPermission(Mockito.any(), Mockito.any()); + Mockito.when(roleService.findRole(Mockito.anyLong())).thenReturn(new RoleVO()); + + Assert.assertEquals((long) accountManagerImpl.createApiKeyAndSecretKey(registerCmdMock).getUserId(), userId); + Mockito.verify(apiKeyPairPermissionsDaoMock, Mockito.times(1)).persist(Mockito.any(ApiKeyPairPermissionVO.class)); + } + + + @Test + public void createApiAndSecretKeyTestWithNonEmptyDates() { + CallContext.register(callingUser, callingAccount); + Mockito.lenient().when(callingUser.getId()).thenReturn(111L); + Date startDate = Date.from(Instant.parse("2024-03-03T10:15:30.00Z")); + Date endDate = Date.from(Instant.parse("2124-03-04T10:15:30.00Z")); + long userId = 111L; + + List> rules = new ArrayList<>(); + rules.add(Map.of( + ApiConstants.RULE, "api", + ApiConstants.PERMISSION, RolePermission.Permission.ALLOW, + ApiConstants.DESCRIPTION, "description" + )); + Mockito.when(registerCmdMock.getRules()).thenReturn(rules); + Mockito.when(registerCmdMock.getUserId()).thenReturn(userId); + Mockito.when(registerCmdMock.getStartDate()).thenReturn(startDate); + Mockito.when(registerCmdMock.getEndDate()).thenReturn(endDate); + Mockito.when(registerCmdMock.getDescription()).thenReturn("key description"); + + UserVO mockUser = new UserVO(userId); + ApiKeyPairPermissionVO permissionVO = new ApiKeyPairPermissionVO(); + ApiKeyPairVO apiKeyPairVO = new ApiKeyPairVO(); + apiKeyPairVO.setUserId(userId); + apiKeyPairVO.setId(1L); + + Mockito.when(userDaoMock.findById(Mockito.anyLong())).thenReturn(userVoMock); + Mockito.when(_accountDao.findById(Mockito.anyLong())).thenReturn(accountVoMock); + Mockito.doNothing().when(accountManagerImpl).checkAccess(Mockito.any(Account.class), Mockito.isNull(), Mockito.anyBoolean(), Mockito.any(Account.class)); + Mockito.doNothing().when(accountManagerImpl).verifyCallerPrivilegeForUserOrAccountOperations(Mockito.any(User.class)); + Mockito.when(apiKeyPairPermissionsDaoMock.persist(Mockito.any(ApiKeyPairPermissionVO.class))).thenReturn(permissionVO); + Mockito.when(apiKeyPairDaoMock.findBySecretKey(Mockito.anyString())).thenReturn(null); + Mockito.doReturn(true).when(roleService).roleHasPermission(Mockito.any(), Mockito.any()); + Mockito.when(roleService.findRole(Mockito.anyLong())).thenReturn(new RoleVO()); + + ApiKeyPair response = accountManagerImpl.createApiKeyAndSecretKey(registerCmdMock); + Mockito.verify(apiKeyPairDaoMock, Mockito.times(1)).persist(Mockito.any(ApiKeyPairVO.class)); + Assert.assertEquals(userId, (long) response.getUserId()); + Assert.assertEquals(response.getStartDate(), startDate); + Assert.assertEquals(response.getEndDate(), endDate); + Assert.assertEquals("key description", response.getDescription()); + } + + @Test(expected = InvalidParameterValueException.class) + public void createApiAndSecretKeyTestWithExpiredDate() { + CallContext.register(callingUser, callingAccount); + Mockito.lenient().when(callingUser.getId()).thenReturn(111L); + + Date startDate = Date.from(Instant.parse("2024-03-01T10:15:30.00Z")); + Date endDate = Date.from(Instant.parse("2024-03-02T10:15:30.00Z")); + long userId = 111L; + + List> rules = new ArrayList<>(); + rules.add(Map.of( + ApiConstants.RULE, "api", + ApiConstants.PERMISSION, RolePermission.Permission.ALLOW, + ApiConstants.DESCRIPTION, "description" + )); + Mockito.when(registerCmdMock.getRules()).thenReturn(rules); + Mockito.when(registerCmdMock.getUserId()).thenReturn(userId); + Mockito.when(registerCmdMock.getStartDate()).thenReturn(startDate); + Mockito.when(registerCmdMock.getEndDate()).thenReturn(endDate); + Mockito.when(registerCmdMock.getDescription()).thenReturn("key description"); + + UserVO mockUser = new UserVO(userId); + ApiKeyPairPermissionVO permissionVO = new ApiKeyPairPermissionVO(); + ApiKeyPairVO apiKeyPairVO = new ApiKeyPairVO(); + apiKeyPairVO.setUserId(userId); + apiKeyPairVO.setId(1L); + + Mockito.when(userDaoMock.findById(Mockito.anyLong())).thenReturn(userVoMock); + Mockito.when(_accountDao.findById(Mockito.anyLong())).thenReturn(accountVoMock); + Mockito.doNothing().when(accountManagerImpl).checkAccess(Mockito.any(Account.class), Mockito.isNull(), Mockito.anyBoolean(), Mockito.any(Account.class)); + Mockito.doNothing().when(accountManagerImpl).verifyCallerPrivilegeForUserOrAccountOperations(Mockito.any(User.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)); + Mockito.doNothing().when(accountManagerImpl).verifyCallerPrivilegeForUserOrAccountOperations(Mockito.any(User.class)); + + ApiKeyPair response = accountManagerImpl.createApiKeyAndSecretKey(registerCmdMock); + Assert.assertEquals(userId, (long) response.getUserId()); + Assert.assertEquals(response.getStartDate(), startDate); + Assert.assertEquals(response.getEndDate(), endDate); + Assert.assertEquals("key description", response.getDescription()); + } + + @Test(expected = InvalidParameterValueException.class) + public void createApiAndSecretKeyTestWithMultipleAllowedPermissionsOneDenied() { + CallContext.register(callingUser, callingAccount); + Mockito.lenient().when(callingUser.getId()).thenReturn(111L); + long userId = 111L; + + List> rules = new ArrayList<>(); + rules.add(Map.of( + ApiConstants.RULE, "api1", + ApiConstants.PERMISSION, RolePermission.Permission.ALLOW, + ApiConstants.DESCRIPTION, "description" + )); + rules.add(Map.of( + ApiConstants.RULE, "api2", + ApiConstants.PERMISSION, RolePermission.Permission.ALLOW, + ApiConstants.DESCRIPTION, "description" + )); + rules.add(Map.of( + ApiConstants.RULE, "api3", + ApiConstants.PERMISSION, RolePermission.Permission.ALLOW, + ApiConstants.DESCRIPTION, "description" + )); + Mockito.when(registerCmdMock.getRules()).thenReturn(rules); + Mockito.when(registerCmdMock.getUserId()).thenReturn(userId); + + UserVO mockUser = new UserVO(userId); + ApiKeyPairVO apiKeyPairVO = new ApiKeyPairVO(); + apiKeyPairVO.setUserId(userId); + apiKeyPairVO.setId(1L); + + Mockito.when(userDaoMock.findById(Mockito.anyLong())).thenReturn(userVoMock); + Mockito.when(_accountDao.findById(Mockito.anyLong())).thenReturn(accountVoMock); + Mockito.doNothing().when(accountManagerImpl).checkAccess(Mockito.any(Account.class), Mockito.isNull(), Mockito.anyBoolean(), Mockito.any(Account.class)); + Mockito.doNothing().when(accountManagerImpl).verifyCallerPrivilegeForUserOrAccountOperations(Mockito.any(User.class)); + Mockito.when(apiKeyPairDaoMock.findBySecretKey(Mockito.anyString())).thenReturn(null); + Mockito.when(roleService.findAllRolePermissionsEntityBy(Mockito.anyLong(), Mockito.anyBoolean())).thenReturn(List.of( + new RolePermissionVO(1L, "api1", RolePermissionEntity.Permission.ALLOW, "description-1"), + new RolePermissionVO(1L, "api2", RolePermissionEntity.Permission.ALLOW, "description-2"), + new RolePermissionVO(1L, "api3", RolePermissionEntity.Permission.DENY, "description-3") + )); + Mockito.when(roleService.findRole(Mockito.anyLong())).thenReturn(new RoleVO()); + + accountManagerImpl.createApiKeyAndSecretKey(registerCmdMock); + Mockito.verify(apiKeyPairDaoMock, Mockito.times(1)).remove(Mockito.anyLong()); + Mockito.verify(apiKeyPairPermissionsDaoMock, Mockito.times(0)).persist(Mockito.any(ApiKeyPairPermissionVO.class)); + } + + @Test(expected = InvalidParameterValueException.class) + public void createApiAndSecretKeyTestWithMultipleAllowedPermissionsOneDoesNotExist() { + CallContext.register(callingUser, callingAccount); + Mockito.lenient().when(callingUser.getId()).thenReturn(111L); + long userId = 111L; + + List> rules = new ArrayList<>(); + rules.add(Map.of( + ApiConstants.RULE, "api1", + ApiConstants.PERMISSION, RolePermission.Permission.ALLOW, + ApiConstants.DESCRIPTION, "description" + )); + rules.add(Map.of( + ApiConstants.RULE, "api2", + ApiConstants.PERMISSION, RolePermission.Permission.ALLOW, + ApiConstants.DESCRIPTION, "description" + )); + rules.add(Map.of( + ApiConstants.RULE, "api3", + ApiConstants.PERMISSION, RolePermission.Permission.DENY, + ApiConstants.DESCRIPTION, "description" + )); + Mockito.when(registerCmdMock.getRules()).thenReturn(rules); + Mockito.when(registerCmdMock.getUserId()).thenReturn(userId); + + ApiKeyPairVO apiKeyPairVO = new ApiKeyPairVO(); + apiKeyPairVO.setUserId(userId); + apiKeyPairVO.setId(1L); + + Mockito.when(userDaoMock.findById(Mockito.anyLong())).thenReturn(userVoMock); + Mockito.when(_accountDao.findById(Mockito.anyLong())).thenReturn(accountVoMock); + Mockito.doNothing().when(accountManagerImpl).checkAccess(Mockito.any(Account.class), Mockito.isNull(), Mockito.anyBoolean(), Mockito.any(Account.class)); + Mockito.doNothing().when(accountManagerImpl).verifyCallerPrivilegeForUserOrAccountOperations(Mockito.any(User.class)); + Mockito.when(apiKeyPairDaoMock.findBySecretKey(Mockito.anyString())).thenReturn(null); + Mockito.when(roleService.findAllRolePermissionsEntityBy(Mockito.anyLong(), Mockito.anyBoolean())).thenReturn(List.of( + new RolePermissionVO(1L, "api1", RolePermissionEntity.Permission.ALLOW, "description-1"), + new RolePermissionVO(1L, "api2", RolePermissionEntity.Permission.ALLOW, "description-2") + )); + Mockito.when(roleService.findRole(Mockito.anyLong())).thenReturn(new RoleVO()); + + accountManagerImpl.createApiKeyAndSecretKey(registerCmdMock); + Mockito.verify(apiKeyPairDaoMock, Mockito.times(0)).remove(Mockito.anyLong()); + Mockito.verify(apiKeyPairPermissionsDaoMock, Mockito.times(0)).persist(Mockito.any(ApiKeyPairPermissionVO.class)); + } + + @Test + public void validateAccountHasAccessToResourceTestValidatesAccessToControlledEntity() { + VMInstanceVO vmInstanceVo = new VMInstanceVO(); + Mockito.doNothing().when(accountManagerImpl).checkAccess(Mockito.any(), Mockito.any(), Mockito.anyBoolean(), Mockito.any()); + + accountManagerImpl.validateAccountHasAccessToResource(callingAccount, AccessType.UseEntry, vmInstanceVo); + + Mockito.verify(accountManagerImpl).checkAccess(callingAccount, AccessType.UseEntry, true, vmInstanceVo); + } + @Test public void testDeleteWebhooksForAccount() { try (MockedStatic mockedComponentContext = Mockito.mockStatic(ComponentContext.class)) { @@ -1554,6 +1952,15 @@ public class AccountManagerImplTest extends AccountManagentImplTestBase { accountManagerImpl.assertUserNotAlreadyInDomain(existingUser, originalAccount); } + @Test + public void deleteApiKeyTestOnePermission() { + Mockito.when(apiKeyPairPermissionsDaoMock.findAllByApiKeyPairId(Mockito.any())).thenReturn(List.of(new ApiKeyPairPermissionVO())); + Mockito.when(userDaoMock.findByIdIncludingRemoved(Mockito.any())).thenReturn(new UserVO()); + accountManagerImpl.deleteApiKey(new ApiKeyPairVO(1L, 1L)); + Mockito.verify(apiKeyPairPermissionsDaoMock, Mockito.times(1)).remove(Mockito.anyLong()); + Mockito.verify(apiKeyPairDaoMock, Mockito.times(1)).remove(Mockito.anyLong()); + } + @Test public void testCheckCallerRoleTypeAllowedToUpdateUserSameAccount() { Mockito.lenient().when(accountManagerImpl.getCurrentCallingAccount()).thenReturn(accountMock); diff --git a/server/src/test/java/com/cloud/vm/UserVmManagerImplTest.java b/server/src/test/java/com/cloud/vm/UserVmManagerImplTest.java index 4edafb3a05a..f7439386fab 100644 --- a/server/src/test/java/com/cloud/vm/UserVmManagerImplTest.java +++ b/server/src/test/java/com/cloud/vm/UserVmManagerImplTest.java @@ -89,6 +89,7 @@ import org.apache.cloudstack.engine.subsystem.api.storage.Scope; import org.apache.cloudstack.engine.subsystem.api.storage.VolumeDataFactory; import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo; import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.framework.jobs.impl.AsyncJobVO; import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; import org.apache.cloudstack.storage.template.VnfTemplateManager; @@ -1823,35 +1824,35 @@ public class UserVmManagerImplTest { Mockito.doReturn(false).when(accountManager).isAdmin(Mockito.anyLong()); Mockito.doReturn(false).when(userVmManagerImpl).getConfigAllowUserExpungeRecoverVm(Mockito.anyLong()); - Assert.assertThrows(PermissionDeniedException.class, () -> userVmManagerImpl.checkExpungeVmPermission(accountMock)); + Assert.assertThrows(PermissionDeniedException.class, () -> userVmManagerImpl.checkExpungeVmPermission(accountMock, null)); } @Test public void checkExpungeVmPermissionTestAccountIsNotAdminConfigTrueNoApiAccessThrowsPermissionDeniedException () { Mockito.doReturn(false).when(accountManager).isAdmin(Mockito.anyLong()); Mockito.doReturn(true).when(userVmManagerImpl).getConfigAllowUserExpungeRecoverVm(Mockito.anyLong()); - doThrow(PermissionDeniedException.class).when(accountManager).checkApiAccess(accountMock, "expungeVirtualMachine"); + doThrow(PermissionDeniedException.class).when(accountManager).checkApiAccess(accountMock, "expungeVirtualMachine", null); - Assert.assertThrows(PermissionDeniedException.class, () -> userVmManagerImpl.checkExpungeVmPermission(accountMock)); + Assert.assertThrows(PermissionDeniedException.class, () -> userVmManagerImpl.checkExpungeVmPermission(accountMock, null)); } @Test public void checkExpungeVmPermissionTestAccountIsNotAdminConfigTrueHasApiAccessReturnNothing () { Mockito.doReturn(false).when(accountManager).isAdmin(Mockito.anyLong()); Mockito.doReturn(true).when(userVmManagerImpl).getConfigAllowUserExpungeRecoverVm(Mockito.anyLong()); - userVmManagerImpl.checkExpungeVmPermission(accountMock); + userVmManagerImpl.checkExpungeVmPermission(accountMock, null); } @Test public void checkExpungeVmPermissionTestAccountIsAdminNoApiAccessThrowsPermissionDeniedException () { Mockito.doReturn(true).when(accountManager).isAdmin(Mockito.anyLong()); - doThrow(PermissionDeniedException.class).when(accountManager).checkApiAccess(accountMock, "expungeVirtualMachine"); + doThrow(PermissionDeniedException.class).when(accountManager).checkApiAccess(accountMock, "expungeVirtualMachine", null); - Assert.assertThrows(PermissionDeniedException.class, () -> userVmManagerImpl.checkExpungeVmPermission(accountMock)); + Assert.assertThrows(PermissionDeniedException.class, () -> userVmManagerImpl.checkExpungeVmPermission(accountMock, null)); } @Test public void checkExpungeVmPermissionTestAccountIsAdminHasApiAccessReturnNothing () { Mockito.doReturn(true).when(accountManager).isAdmin(Mockito.anyLong()); - userVmManagerImpl.checkExpungeVmPermission(accountMock); + userVmManagerImpl.checkExpungeVmPermission(accountMock, null); } @Test @@ -3661,7 +3662,7 @@ public class UserVmManagerImplTest { when(callingAccount.getId()).thenReturn(accountId); when(callContext.getCallingAccount()).thenReturn(callingAccount); when(accountManager.isAdmin(callingAccount.getId())).thenReturn(true); - doNothing().when(accountManager).checkApiAccess(callingAccount, BaseCmd.getCommandNameByClass(ExpungeVMCmd.class)); + doNothing().when(accountManager).checkApiAccess(callingAccount, BaseCmd.getCommandNameByClass(ExpungeVMCmd.class), null); try (MockedStatic mockedCallContext = mockStatic(CallContext.class)) { mockedCallContext.when(CallContext::current).thenReturn(callContext); mockedCallContext.when(() -> CallContext.register(callContext, ApiCommandResourceType.Volume)).thenReturn(callContext); @@ -3671,6 +3672,9 @@ public class UserVmManagerImplTest { when(cmd.getExpunge()).thenReturn(expunge); List volumeIds = List.of(volumeId); when(cmd.getVolumeIds()).thenReturn(volumeIds); + AsyncJobVO asyncJobMock = mock(AsyncJobVO.class); + when(cmd.getJob()).thenReturn(asyncJobMock); + when(asyncJobMock.getCmdInfo()).thenReturn("{}"); UserVmVO vm = mock(UserVmVO.class); when(vm.getId()).thenReturn(vmId); 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 5d9ee268d8b..7b070d4c7cb 100644 --- a/server/src/test/java/org/apache/cloudstack/acl/RoleManagerImplTest.java +++ b/server/src/test/java/org/apache/cloudstack/acl/RoleManagerImplTest.java @@ -21,7 +21,6 @@ import com.cloud.user.Account; import com.cloud.user.AccountManager; import com.cloud.utils.Pair; -import org.apache.cloudstack.acl.RolePermissionEntity.Permission; import org.apache.cloudstack.acl.dao.RoleDao; import org.apache.cloudstack.acl.dao.RolePermissionsDao; import org.apache.commons.collections.CollectionUtils; @@ -58,6 +57,10 @@ public class RoleManagerImplTest { @Mock private RolePermission rolePermission2Mock; @Mock + private RolePermission rolePermission2MockWithDeniedPermission; + @Mock + private RolePermission rolePermission3Mock; + @Mock private Account callerAccountMock; @Mock private Role callerAccountRoleMock; @@ -76,7 +79,7 @@ public class RoleManagerImplTest { private RoleVO roleVoMock; private long roleMockId = 1l; - private Map rolePermissions = new HashMap<>(); + private Map rolePermissions = new HashMap<>(); public void setUpRoleVisibilityTests() { Mockito.doReturn(List.of("api1", "api2", "api3")).when(accountManagerMock).getApiNameList(); @@ -89,18 +92,11 @@ public class RoleManagerImplTest { Mockito.when(rolePermission2Mock.getRule()).thenReturn(new Rule("api2")); Mockito.doReturn(RolePermissionEntity.Permission.ALLOW).when(rolePermission1Mock).getPermission(); Mockito.doReturn(RolePermissionEntity.Permission.ALLOW).when(rolePermission2Mock).getPermission(); + Mockito.doReturn(RolePermissionEntity.Permission.DENY).when(rolePermission2MockWithDeniedPermission).getPermission(); - List lessPermissionsRolePermissions = Collections.singletonList(rolePermission1Mock); Mockito.doReturn(1L).when(lessPermissionsRoleMock).getId(); - Mockito.when(roleManagerImpl.findAllPermissionsBy(1L)).thenReturn(lessPermissionsRolePermissions); - - List morePermissionsRolePermissions = List.of(rolePermission1Mock, rolePermission2Mock); Mockito.doReturn(2L).when(morePermissionsRoleMock).getId(); - Mockito.when(roleManagerImpl.findAllPermissionsBy(morePermissionsRoleMock.getId())).thenReturn(morePermissionsRolePermissions); - - List differentPermissionsRolePermissions = Collections.singletonList(rolePermission2Mock); Mockito.doReturn(3L).when(differentPermissionsRoleMock).getId(); - Mockito.when(roleManagerImpl.findAllPermissionsBy(differentPermissionsRoleMock.getId())).thenReturn(differentPermissionsRolePermissions); } @Before @@ -225,9 +221,6 @@ public class RoleManagerImplTest { setUpRoleVisibilityTests(); List roles = new ArrayList<>(); - List callerAccountRolePermissions = List.of(rolePermission1Mock, rolePermission2Mock); - Mockito.when(roleManagerImpl.findAllPermissionsBy(callerAccountRoleMock.getId())).thenReturn(callerAccountRolePermissions); - roles.add(callerAccountRoleMock); roles.add(lessPermissionsRoleMock); @@ -243,9 +236,11 @@ public class RoleManagerImplTest { 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(), false)).thenReturn(callerAccountRolePermissions); + List callerAccountRolePermissions2 = Collections.singletonList(rolePermission2Mock); + Mockito.when(roleManagerImpl.findAllRolePermissionsEntityBy(morePermissionsRoleMock.getId(), false)).thenReturn(callerAccountRolePermissions2); roles.add(callerAccountRoleMock); roles.add(morePermissionsRoleMock); @@ -261,8 +256,11 @@ public class RoleManagerImplTest { 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(), false)).thenReturn(callerAccountRolePermissions); + + List callerAccountRolePermissions3 = Collections.singletonList(rolePermission2Mock); + Mockito.when(roleManagerImpl.findAllRolePermissionsEntityBy(differentPermissionsRoleMock.getId(), false)).thenReturn(callerAccountRolePermissions3); roles.add(callerAccountRoleMock); roles.add(differentPermissionsRoleMock); @@ -276,10 +274,10 @@ public class RoleManagerImplTest { @Test public void roleHasPermissionTestRoleWithMoreAndSamePermissionsReturnsTrue() { setUpRoleVisibilityTests(); - rolePermissions.put("api1", Permission.ALLOW); - rolePermissions.put("api2", Permission.ALLOW); + rolePermissions.put("api1", rolePermission1Mock); + rolePermissions.put("api2", rolePermission2Mock); - boolean result = roleManagerImpl.roleHasPermission(rolePermissions, lessPermissionsRoleMock); + boolean result = roleManagerImpl.roleHasPermission(rolePermissions, Collections.singletonList(rolePermission1Mock)); Assert.assertTrue(result); } @@ -287,10 +285,10 @@ public class RoleManagerImplTest { @Test public void roleHasPermissionTestRoleAllowedApisDoesNotContainRoleToAccessAllowedApiReturnsFalse() { setUpRoleVisibilityTests(); - rolePermissions.put("api2", Permission.ALLOW); - rolePermissions.put("api3", Permission.ALLOW); + rolePermissions.put("api2", rolePermission2Mock); + rolePermissions.put("api3", rolePermission3Mock); - boolean result = roleManagerImpl.roleHasPermission(rolePermissions, morePermissionsRoleMock); + boolean result = roleManagerImpl.roleHasPermission(rolePermissions, List.of(rolePermission1Mock, rolePermission2Mock)); Assert.assertFalse(result); } @@ -298,10 +296,10 @@ public class RoleManagerImplTest { @Test public void roleHasPermissionTestRolePermissionsDeniedApiContainRoleToAccessAllowedApiReturnsFalse() { setUpRoleVisibilityTests(); - rolePermissions.put("api1", Permission.ALLOW); - rolePermissions.put("api2", Permission.DENY); + rolePermissions.put("api1", rolePermission1Mock); + rolePermissions.put("api2", rolePermission2MockWithDeniedPermission); - boolean result = roleManagerImpl.roleHasPermission(rolePermissions, morePermissionsRoleMock); + boolean result = roleManagerImpl.roleHasPermission(rolePermissions, List.of(rolePermission1Mock, rolePermission2Mock)); Assert.assertFalse(result); } @@ -310,11 +308,11 @@ public class RoleManagerImplTest { 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); - Assert.assertEquals(roleRulesAndPermissions.get("api2"), Permission.ALLOW); + Assert.assertEquals(roleRulesAndPermissions.get("api1"), rolePermission1Mock); + Assert.assertEquals(roleRulesAndPermissions.get("api2"), rolePermission2Mock); } @Test diff --git a/test/integration/smoke/test_accounts.py b/test/integration/smoke/test_accounts.py index 209c8431b23..d30f9f39890 100644 --- a/test/integration/smoke/test_accounts.py +++ b/test/integration/smoke/test_accounts.py @@ -1744,7 +1744,7 @@ class TestUserAPIKeys(cloudstackTestCase): self.apiclient, id=self.account_2.id )[0].user - with self.assertRaises(CloudstackAPIException) as e: + with self.assertRaises(Exception) as e: User.registerUserKeys(cs_api, users[0].id) diff --git a/tools/marvin/marvin/cloudstackTestClient.py b/tools/marvin/marvin/cloudstackTestClient.py index 8c5a0d6e612..fb2afc19d2e 100644 --- a/tools/marvin/marvin/cloudstackTestClient.py +++ b/tools/marvin/marvin/cloudstackTestClient.py @@ -148,32 +148,24 @@ class CSTestClient(object): "Client Creation Failed") return FAILED - getuser_keys = getUserKeys.getUserKeysCmd() - getuser_keys.id = list_user_res[0].id - getuser_keys_res = self.__apiClient.getUserKeys(getuser_keys) - if getuser_keys_res is None : - self.__logger.error("__createApiClient: API " - "Client Creation Failed") - return FAILED - - api_key = getuser_keys_res.apikey - security_key = getuser_keys_res.secretkey - user_id = list_user_res[0].id - if api_key is None: + getuser_keys = getUserKeys.getUserKeysCmd() + getuser_keys.id = user_id + getuser_keys_res = self.__apiClient.getUserKeys(getuser_keys) + + if getuser_keys_res['apikey'] is None: ret = self.__getKeys(user_id) - if ret != FAILED: - mgmt_details.apiKey = ret[0] - mgmt_details.securityKey = ret[1] - else: + if ret == FAILED: self.__logger.error("__createApiClient: API Client " "Creation Failed while " "Registering User") return FAILED + mgmt_details.apiKey = ret[0] + mgmt_details.securityKey = ret[1] else: mgmt_details.port = 8080 - mgmt_details.apiKey = api_key - mgmt_details.securityKey = security_key + mgmt_details.apiKey = getuser_keys_res['apikey'] + mgmt_details.securityKey = getuser_keys_res['secretkey'] ''' Now Create the Connection objects and Api Client using new details @@ -216,6 +208,7 @@ class CSTestClient(object): try: register_user = registerUserKeys.registerUserKeysCmd() register_user.id = userid + register_user.name = f"keypair-{userid}" register_user_res = \ self.__apiClient.registerUserKeys(register_user) if not register_user_res: @@ -224,13 +217,13 @@ class CSTestClient(object): getuser_keys = getUserKeys.getUserKeysCmd() getuser_keys.id = userid getuser_keys_res = self.__apiClient.getUserKeys(getuser_keys) - if getuser_keys_res is None : + if getuser_keys_res is None: self.__logger.error("__createApiClient: API " - "Client Creation Failed") + "Client Creation Failed") return FAILED - api_key = getuser_keys_res.apikey - security_key = getuser_keys_res.secretkey + api_key = getuser_keys_res['apikey'] + security_key = getuser_keys_res['secretkey'] return (api_key, security_key) except Exception as e: self.__logger.exception("Exception Occurred Under __geKeys : " diff --git a/tools/marvin/marvin/lib/base.py b/tools/marvin/marvin/lib/base.py index 2405d54162a..825159a2e53 100755 --- a/tools/marvin/marvin/lib/base.py +++ b/tools/marvin/marvin/lib/base.py @@ -335,6 +335,7 @@ class User: def registerUserKeys(cls, apiclient, userid): cmd = registerUserKeys.registerUserKeysCmd() cmd.id = userid + cmd.name = f"keypair-{userid}" return apiclient.registerUserKeys(cmd) def update(self, apiclient, **kwargs):