API key pair restructure (#9504)
Co-authored-by: Bernardo De Marco Gonçalves <bernardomg2004@gmail.com>
diff --git a/api/src/main/java/com/cloud/event/EventTypes.java b/api/src/main/java/com/cloud/event/EventTypes.java
index 889e821..6524d9d 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 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 eb47b75..30919e5 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 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.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 @@
void markUserRegistered(long userId);
- public String[] createApiKeyAndSecretKey(RegisterUserKeyCmd cmd);
+ ApiKeyPair createApiKeyAndSecretKey(RegisterUserKeysCmd cmd);
public String[] createApiKeyAndSecretKey(final long userId);
@@ -125,6 +134,8 @@
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 @@
*/
UserAccount getUserAccountById(Long userId);
- public Pair<Boolean, Map<String, String>> getKeys(GetUserKeysCmd cmd);
+ Pair<Boolean, Map<String, String>> getKeys(GetUserKeysCmd cmd);
- public Pair<Boolean, Map<String, String>> getKeys(Long userId);
+ ListResponse<ApiKeyPairResponse> listKeys(ListUserKeysCmd cmd);
+
+ List<ApiKeyPairPermission> listKeyRules(ListUserKeyRulesCmd cmd);
+
+ void deleteApiKey(DeleteUserKeysCmd cmd);
+
+ void deleteApiKey(ApiKeyPair id);
/**
* Lists user two-factor authentication provider plugins
@@ -151,4 +168,13 @@
*/
UserTwoFactorAuthenticator getUserTwoFactorAuthenticationProvider(final Long domainId);
+ ApiKeyPair getLatestUserKeyPair(Long userId);
+
+ ApiKeyPair getKeyPairById(Long id);
+
+ ApiKeyPair getKeyPairByApiKey(String apiKey);
+
+ String getAccessingApiKey(BaseCmd cmd);
+
+ List<RolePermissionEntity> 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 0000000..63405c6
--- /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 041b39a..da7245a 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 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 e6b07fb..5736244 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 @@
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 660f64f..286a359 100644
--- a/api/src/main/java/org/apache/cloudstack/acl/APIChecker.java
+++ b/api/src/main/java/org/apache/cloudstack/acl/APIChecker.java
@@ -20,6 +20,7 @@
import com.cloud.user.Account;
import com.cloud.user.User;
import com.cloud.utils.component.Adapter;
+import org.apache.cloudstack.acl.apikeypair.ApiKeyPairPermission;
import java.util.List;
@@ -31,8 +32,8 @@
// 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 @@
*/
List<String> getApisAllowedToUser(Role role, User user, List<String> apiNames) throws PermissionDeniedException;
boolean isEnabled();
+ List<RolePermissionEntity> 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 251c6b6..f382b1c 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.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 f041c83..14e0a60 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 @@
List<RolePermission> findAllPermissionsBy(Long roleId);
+ List<RolePermissionEntity> findAllRolePermissionsEntityBy(Long roleId, boolean considerImplicitRules);
+
Permission getRolePermission(String permission);
+
+ int removeRolesIfNeeded(List<? extends Role> 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<String, RolePermissionEntity> rolePermissions, List<RolePermissionEntity> 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<String, RolePermissionEntity> getRoleRulesAndPermissions(List<RolePermissionEntity> 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 a4ef777..ad01825 100644
--- a/api/src/main/java/org/apache/cloudstack/acl/Rule.java
+++ b/api/src/main/java/org/apache/cloudstack/acl/Rule.java
@@ -25,16 +25,18 @@
public final class Rule {
private final String rule;
+ private final Pattern matchingPattern;
private final static Pattern ALLOWED_PATTERN = Pattern.compile("^[a-zA-Z0-9*]+$");
public Rule(final String rule) {
validate(rule);
this.rule = rule;
+ matchingPattern = Pattern.compile(rule.toLowerCase().replace("*", "(\\w*\\*?)+"));
}
public boolean matches(final String commandName) {
- return StringUtils.isNotEmpty(commandName)
- && commandName.toLowerCase().matches(rule.toLowerCase().replace("*", "\\w*"));
+ return StringUtils.isNotEmpty(commandName) &&
+ matchingPattern.matcher(commandName.toLowerCase()).matches();
}
public String getRuleString() {
diff --git a/api/src/main/java/org/apache/cloudstack/acl/apikeypair/ApiKeyPair.java b/api/src/main/java/org/apache/cloudstack/acl/apikeypair/ApiKeyPair.java
new file mode 100644
index 0000000..ecce0ae
--- /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 0000000..60b3834
--- /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 0000000..de9c829
--- /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<ApiKeyPairPermission> 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 9a8913d..25ba233 100644
--- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java
+++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java
@@ -19,6 +19,7 @@
public class ApiConstants {
public static final String ACCOUNT = "account";
public static final String ACCOUNTS = "accounts";
+ public static final String ACCOUNT_NAME = "accountname";
public static final String ACCOUNT_TYPE = "accounttype";
public static final String ACCOUNT_ID = "accountid";
public static final String ACCOUNT_IDS = "accountids";
@@ -46,6 +47,7 @@
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 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 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 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 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 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 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 6859b0a..c67c5a0 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 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 f30f6f3..e495cf2 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 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 @@
@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 8e92e87..338c1e7 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 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.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.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 @@
GuiThemeResponse createGuiThemeResponse(GuiThemeJoin guiThemeJoin);
ConsoleSessionResponse createConsoleSessionResponse(ConsoleSession consoleSession, ResponseView responseView);
+
+ ApiKeyPairResponse createKeyPairResponse(ApiKeyPair keyPair);
+
+ ListResponse<BaseRolePermissionResponse> createKeypairPermissionsResponse(List<ApiKeyPairPermission> 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 2fe3c7c..75fcf12 100644
--- a/api/src/main/java/org/apache/cloudstack/api/command/admin/storage/AddImageStoreS3CMD.java
+++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/storage/AddImageStoreS3CMD.java
@@ -27,7 +27,7 @@
import static org.apache.cloudstack.api.ApiConstants.S3_HTTPS_FLAG;
import static org.apache.cloudstack.api.ApiConstants.S3_MAX_ERROR_RETRY;
import static org.apache.cloudstack.api.ApiConstants.S3_SIGNER;
-import static org.apache.cloudstack.api.ApiConstants.S3_SECRET_KEY;
+import static org.apache.cloudstack.api.ApiConstants.SECRET_KEY;
import static org.apache.cloudstack.api.ApiConstants.S3_SOCKET_TIMEOUT;
import static org.apache.cloudstack.api.ApiConstants.S3_USE_TCP_KEEPALIVE;
import static org.apache.cloudstack.api.BaseCmd.CommandType.BOOLEAN;
@@ -64,7 +64,7 @@
@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 @@
Map<String, String> 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 0000000..6cf5551
--- /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 f04c5f6..a651bef 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 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<Boolean, Map<String, String>> 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 0000000..4345cae
--- /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<ApiKeyPairPermission> permissions = _accountService.listKeyRules(this);
+ ListResponse<BaseRolePermissionResponse> 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 0000000..ded05d6
--- /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<ApiKeyPairResponse> 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 61f47e2..0000000
--- 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 0000000..11d7c1d
--- /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<Map<String, Object>> getRules() {
+ List<Map<String, Object>> rulesDetails = new ArrayList<>();
+
+ if (rules == null) {
+ return rulesDetails;
+ }
+
+ for (Object ruleObject : rules.values()) {
+ HashMap<String, String> detail = (HashMap<String, String>) ruleObject;
+ Map<String, Object> 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<Long> 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 628ddb9..3f5ce24 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 @@
//////////////// 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 @@
@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 String getApiKey() {
- return apiKey;
+ return userApiKey;
}
public String getEmail() {
@@ -127,7 +127,7 @@
}
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 0000000..350d71b
--- /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<BaseRolePermissionResponse> 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<BaseRolePermissionResponse> getPermissions() {
+ return permissions;
+ }
+
+ public void setPermissions(List<BaseRolePermissionResponse> 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 3b1c1a1..61b025b 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 @@
@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 5cd67ff..5b053aa 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 @@
ListResponse<UserResponse> searchForUsers(Long domainId, boolean recursive) throws PermissionDeniedException;
+ List<Long> searchForAccessibleUsers();
+
ListResponse<EventResponse> searchForEvents(ListEventsCmd cmd);
ListResponse<ResourceTagResponse> 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 79e6127..b99ba48 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<String> apiNames;
+ private static List<Rule> apiRules;
+ private static ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false);
+
+ @BeforeClass
+ public static void setup() {
+ provider.addIncludeFilter(new AnnotationTypeFilter(APICommand.class));
+ Set<BeanDefinition> beanDefinitions = provider.findCandidateComponents("org.apache.cloudstack.api");
+
+ apiNames = new ArrayList<>();
+ apiRules = new ArrayList<>();
+ for(BeanDefinition bd : beanDefinitions) {
+ if (bd instanceof AnnotatedBeanDefinition) {
+ Map<String, Object> 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 @@
}
@Test
- public void testMatchesEmpty() throws Exception {
- Rule rule = new Rule("someString");
- Assert.assertFalse(rule.matches(""));
+ public void ruleMatchesTestNoMatchesOnEmptyString() throws Exception {
+ String testCmd = "";
+ List<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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 @@
}
@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 @@
}
@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 a78f93f..5c47087 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 @@
Map<String, String> detailMap = new HashMap<String, String>();
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 c5ca410..7345eeb 100644
--- a/engine/schema/src/main/java/com/cloud/user/UserAccountVO.java
+++ b/engine/schema/src/main/java/com/cloud/user/UserAccountVO.java
@@ -36,7 +36,6 @@
import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils;
import org.apache.commons.lang3.StringUtils;
-import com.cloud.utils.db.Encrypt;
import com.cloud.utils.db.GenericDao;
@Entity
@@ -69,13 +68,6 @@
@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;
@@ -204,24 +196,6 @@
}
@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 d74aa7e..1b89bc2 100644
--- a/engine/schema/src/main/java/com/cloud/user/UserVO.java
+++ b/engine/schema/src/main/java/com/cloud/user/UserVO.java
@@ -33,7 +33,6 @@
import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils;
import com.cloud.user.Account.State;
-import com.cloud.utils.db.Encrypt;
import com.cloud.utils.db.GenericDao;
import org.apache.commons.lang3.StringUtils;
@@ -71,13 +70,6 @@
@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 @@
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());
@@ -244,26 +234,6 @@
}
@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)) {
return "UTC";
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 de3b769..e377bba 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 @@
List<UserAccountVO> 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 c9de9a3..28392ab 100644
--- a/engine/schema/src/main/java/com/cloud/user/dao/UserAccountDaoImpl.java
+++ b/engine/schema/src/main/java/com/cloud/user/dao/UserAccountDaoImpl.java
@@ -19,7 +19,6 @@
import com.cloud.user.UserAccount;
import com.cloud.user.UserAccountVO;
import com.cloud.utils.db.GenericDaoBase;
-import com.cloud.utils.db.SearchBuilder;
import com.cloud.utils.db.SearchCriteria;
import org.springframework.stereotype.Component;
@@ -28,14 +27,6 @@
@Component
public class UserAccountDaoImpl extends GenericDaoBase<UserAccountVO, Long> implements UserAccountDao {
- protected final SearchBuilder<UserAccountVO> userAccountSearch;
-
- public UserAccountDaoImpl() {
- userAccountSearch = createSearchBuilder();
- userAccountSearch.and("apiKey", userAccountSearch.entity().getApiKey(), SearchCriteria.Op.EQ);
- userAccountSearch.done();
- }
-
@Override
public List<UserAccountVO> getAllUsersByNameAndEntity(String username, String entity) {
if (username == null) {
@@ -79,12 +70,4 @@
}
return false;
}
-
- @Override
- public UserAccount getUserByApiKey(String apiKey) {
- SearchCriteria<UserAccountVO> 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 14b0742..2e160ef 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
@@ -38,13 +38,6 @@
List<UserVO> 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
* @return
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 8baf732..de60e48 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 @@
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();
@@ -122,13 +118,6 @@
}
@Override
- public UserVO findUserBySecretKey(String secretKey) {
- SearchCriteria<UserVO> sc = SecretKeySearch.create();
- sc.setParameters("secretKey", secretKey);
- return findOneBy(sc);
- }
-
- @Override
public UserVO findUserByRegistrationToken(String registrationToken) {
SearchCriteria<UserVO> sc = RegistrationTokenSearch.create();
sc.setParameters("registrationToken", registrationToken);
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 0000000..7972fe6
--- /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 0000000..eb38b08
--- /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 f3347ab..588fa02 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 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 0000000..006c2af
--- /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, Long> {
+ ApiKeyPairVO findBySecretKey(String secretKey);
+
+ ApiKeyPairVO findByApiKey(String apiKey);
+
+ ApiKeyPairVO findByUuid(String uuid);
+
+ Pair<List<ApiKeyPairVO>, Integer> listApiKeysByUserOrApiKeyId(Long userId, Long apiKeyId);
+
+ ApiKeyPairVO getLastApiKeyCreatedByUser(Long userId);
+
+ Pair<List<ApiKeyPairVO>, Integer> listByUserIdsPaginated(List<Long> 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 0000000..a4895ef
--- /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<ApiKeyPairVO, Long> 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<ApiKeyPairVO> 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<ApiKeyPairVO> sc = keyPairSearch.create();
+ sc.setParameters(API_KEY, apiKey);
+ return findOneBy(sc);
+ }
+
+ public ApiKeyPairVO findBySecretKey(String secretKey) {
+ SearchCriteria<ApiKeyPairVO> sc = keyPairSearch.create();
+ sc.setParameters(SECRET_KEY, secretKey);
+ return findOneBy(sc);
+ }
+
+ public Pair<List<ApiKeyPairVO>, Integer> listApiKeysByUserOrApiKeyId(Long userId, Long apiKeyId) {
+ SearchCriteria<ApiKeyPairVO> 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<ApiKeyPairVO> 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<List<ApiKeyPairVO>, Integer> listByUserIdsPaginated(List<Long> userIds, ListUserKeysCmd cmd) {
+ Long pageSizeVal = cmd.getPageSizeVal();
+ Long startIndex = cmd.getStartIndex();
+ Filter searchFilter = new Filter(ApiKeyPairVO.class, ID, true, startIndex, pageSizeVal);
+
+ final SearchCriteria<ApiKeyPairVO> sc = keyPairSearch.create();
+ sc.setParameters(USER_ID, (Object[]) userIds.toArray(new Long[0]));
+
+ Pair<List<ApiKeyPairVO>, 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 0000000..cbca2fd
--- /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<ApiKeyPairPermissionVO, Long> {
+ List<ApiKeyPairPermissionVO> findAllByApiKeyPairId(Long apiKeyPairId);
+
+ List<ApiKeyPairPermissionVO> 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 0000000..4510e0a
--- /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<ApiKeyPairPermissionVO, Long> implements ApiKeyPairPermissionsDao {
+ private static final String API_KEY_PAIR_ID = "apiKeyPairId";
+ private static final String SORT_ORDER = "sortOrder";
+
+ private final SearchBuilder<ApiKeyPairPermissionVO> permissionByApiKeyPairIdSearch;
+
+ public ApiKeyPairPermissionsDaoImpl() {
+ super();
+
+ permissionByApiKeyPairIdSearch = createSearchBuilder();
+ permissionByApiKeyPairIdSearch.and(API_KEY_PAIR_ID, permissionByApiKeyPairIdSearch.entity().getApiKeyPairId(), SearchCriteria.Op.EQ);
+ permissionByApiKeyPairIdSearch.done();
+ }
+
+ public List<ApiKeyPairPermissionVO> findAllByApiKeyPairId(Long apiKeyPairId) {
+ SearchCriteria<ApiKeyPairPermissionVO> 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<ApiKeyPairPermissionVO> 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<ApiKeyPairPermissionVO> findAllByKeyPairIdSorted(Long apiKeyPairId) {
+ final SearchCriteria<ApiKeyPairPermissionVO> sc = permissionByApiKeyPairIdSearch.create();
+ sc.setParameters(API_KEY_PAIR_ID, apiKeyPairId);
+ final Filter searchBySorted = new Filter(ApiKeyPairPermissionVO.class, SORT_ORDER, true, null, null);
+ final List<ApiKeyPairPermissionVO> 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 ec40dc0..d7e88bd 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 @@
for (ImageStoreDetailVO detail : details) {
String name = detail.getName();
String value = detail.getValue();
- if (name.equals(ApiConstants.KEY) || name.equals(ApiConstants.S3_SECRET_KEY)) {
+ if (name.equals(ApiConstants.KEY) || name.equals(ApiConstants.SECRET_KEY)) {
value = DBEncryptionUtil.decrypt(value);
}
detailsMap.put(name, value);
diff --git a/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml b/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml
index 0656d5e..edc14d9 100644
--- a/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml
+++ b/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml
@@ -310,4 +310,6 @@
<bean id="gpuDeviceDaoImpl" class="com.cloud.gpu.dao.GpuDeviceDaoImpl" />
<bean id="vgpuProfileDaoImpl" class="com.cloud.gpu.dao.VgpuProfileDaoImpl" />
<bean id="importVMTaskDaoImpl" class="com.cloud.vm.dao.ImportVMTaskDaoImpl" />
+ <bean id="apiKeyPairDaoImpl" class="org.apache.cloudstack.acl.dao.ApiKeyPairDaoImpl" />
+ <bean id="apiKeyPairPermissionsDaoImpl" class="org.apache.cloudstack.acl.dao.ApiKeyPairPermissionsDaoImpl" />
</beans>
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 d330ecd..7923ef8 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 @@
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 340cfa9..dcba71e 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 @@
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 dbb606b..51edd62 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 @@
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 030e0bc..f3e2335 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 @@
private RoleService roleService;
private List<PluggableService> services;
- private Map<RoleType, Set<String>> annotationRoleBasedApisMap = new HashMap<RoleType, Set<String>>();
+ private Map<RoleType, Set<String>> annotationRoleBasedApisMap = new HashMap<>();
private LazyCache<Long, Account> accountCache;
private LazyCache<Long, Pair<Role, List<RolePermission>>> rolePermissionsCache;
@@ -56,7 +59,7 @@
protected DynamicRoleBasedAPIAccessChecker() {
super();
for (RoleType roleType : RoleType.values()) {
- annotationRoleBasedApisMap.put(roleType, new HashSet<String>());
+ annotationRoleBasedApisMap.put(roleType, new HashSet<>());
}
}
@@ -67,9 +70,12 @@
}
List<RolePermission> allPermissions = roleService.findAllPermissionsBy(role.getId());
+ List<RolePermissionEntity> allPermissionEntities = allPermissions.stream().map(permission -> (RolePermissionEntity) permission)
+ .collect(Collectors.toList());
+
List<String> 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 @@
* @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<RolePermission> allPermissions) {
- for (final RolePermission permission : allPermissions) {
+ public boolean checkApiPermissionByRole(Role role, String apiName, List<RolePermissionEntity> allPermissions, boolean keyPairOverride) {
+ for (RolePermissionEntity permission : allPermissions) {
if (!permission.getRule().matches(apiName)) {
continue;
}
@@ -94,13 +100,13 @@
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 @@
}
@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<Role, List<RolePermission>> 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<RolePermission> 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<Role, List<RolePermission>> 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<RolePermission> allPermissions = roleService.findAllPermissionsBy(accountRole.getId());
- if (checkApiPermissionByRole(accountRole, commandName, allPermissions)) {
+ boolean considerKeyPairPermissions = apiKeyPairPermissions.length > 0;
+ List<RolePermissionEntity> 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<RolePermissionEntity> 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 e58be3a..71fbbb4 100644
--- a/plugins/acl/dynamic-role-based/src/test/java/org/apache/cloudstack/acl/DynamicRoleBasedAPIAccessCheckerTest.java
+++ b/plugins/acl/dynamic-role-based/src/test/java/org/apache/cloudstack/acl/DynamicRoleBasedAPIAccessCheckerTest.java
@@ -22,6 +22,8 @@
import java.util.Collections;
import java.util.List;
+import com.cloud.exception.UnavailableCommandException;
+import org.apache.cloudstack.acl.apikeypair.ApiKeyPairPermission;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
@@ -195,4 +197,68 @@
List<String> 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 2e7ae23..8513f45 100644
--- a/plugins/acl/project-role-based/src/main/java/org/apache/cloudstack/acl/ProjectRoleBasedApiAccessChecker.java
+++ b/plugins/acl/project-role-based/src/main/java/org/apache/cloudstack/acl/ProjectRoleBasedApiAccessChecker.java
@@ -23,6 +23,7 @@
import javax.naming.ConfigurationException;
import org.apache.cloudstack.acl.RolePermissionEntity.Permission;
+import org.apache.cloudstack.acl.apikeypair.ApiKeyPairPermission;
import org.apache.cloudstack.context.CallContext;
import com.cloud.exception.PermissionDeniedException;
@@ -105,7 +106,7 @@
}
@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 @@
}
@Override
- public boolean checkAccess(Account account, String apiCommandName) throws PermissionDeniedException {
+ public boolean checkAccess(Account account, String apiCommandName, ApiKeyPairPermission... apiKeyPairPermissions) throws PermissionDeniedException {
return true;
}
@@ -183,6 +184,11 @@
}
@Override
+ public List<RolePermissionEntity> 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 3444f96..6cf4da8 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.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 @@
}
@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 @@
}
@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 @@
return super.start();
}
+ @Override
+ public List<RolePermissionEntity> getImplicitRolePermissions(RoleType roleType) {
+ return annotationRoleBasedApisMap.get(roleType)
+ .stream()
+ .map(implicitApi -> new RolePermissionBaseVO(implicitApi, RolePermissionEntity.Permission.ALLOW))
+ .collect(Collectors.toList());
+ }
+
private void processMapping(Map<String, String> configMap) {
for (Map.Entry<String, String> entry : configMap.entrySet()) {
String apiName = entry.getKey();
diff --git a/plugins/api/discovery/pom.xml b/plugins/api/discovery/pom.xml
index b0f2961..1992da8 100644
--- a/plugins/api/discovery/pom.xml
+++ b/plugins/api/discovery/pom.xml
@@ -39,6 +39,12 @@
<artifactId>cloud-utils</artifactId>
<version>${project.version}</version>
</dependency>
+ <dependency>
+ <groupId>org.apache.cloudstack</groupId>
+ <artifactId>cloud-plugin-api-limit-account-based</artifactId>
+ <version>${project.version}</version>
+ <scope>compile</scope>
+ </dependency>
</dependencies>
<build>
<plugins>
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 9c01435..8cd3193 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 void execute() throws ServerApiException {
if (_apiDiscoveryService != null) {
User user = CallContext.current().getCallingUser();
- ListResponse<ApiDiscoveryResponse> response = (ListResponse<ApiDiscoveryResponse>)_apiDiscoveryService.listApis(user, name);
+ ListResponse<ApiDiscoveryResponse> response = (ListResponse<ApiDiscoveryResponse>)_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 5a6eab7..073010a 100644
--- a/plugins/api/discovery/src/main/java/org/apache/cloudstack/discovery/ApiDiscoveryService.java
+++ b/plugins/api/discovery/src/main/java/org/apache/cloudstack/discovery/ApiDiscoveryService.java
@@ -18,6 +18,7 @@
import com.cloud.user.Account;
import org.apache.cloudstack.api.BaseResponse;
+import org.apache.cloudstack.api.command.user.discovery.ListApisCmd;
import org.apache.cloudstack.api.response.ListResponse;
import com.cloud.user.User;
@@ -28,5 +29,5 @@
public interface ApiDiscoveryService extends PluggableService {
List<String> listApiNames(Account account);
- ListResponse<? extends BaseResponse> listApis(User user, String apiName);
+ ListResponse<? extends BaseResponse> 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 4493f1e..d412f12 100644
--- a/plugins/api/discovery/src/main/java/org/apache/cloudstack/discovery/ApiDiscoveryServiceImpl.java
+++ b/plugins/api/discovery/src/main/java/org/apache/cloudstack/discovery/ApiDiscoveryServiceImpl.java
@@ -26,6 +26,7 @@
import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.stream.Collectors;
import javax.inject.Inject;
@@ -33,6 +34,8 @@
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.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 @@
@Inject
RoleService roleService;
+ @Inject
+ ApiKeyPairService apiKeyPairService;
+
protected ApiDiscoveryServiceImpl() {
super();
}
@@ -257,16 +264,24 @@
}
@Override
- public ListResponse<? extends BaseResponse> listApis(User user, String name) {
+ public ListResponse<? extends BaseResponse> listApis(User user, String name, ListApisCmd cmd) {
ListResponse<ApiDiscoveryResponse> response = new ListResponse<>();
List<ApiDiscoveryResponse> responseList = new ArrayList<>();
List<String> 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 @@
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 @@
return cmdList;
}
+ protected List<ApiDiscoveryResponse> listApisForKeyPair(String apiKey, String apiName, User user, Role role, List<String> apisAllowed) {
+ List<RolePermissionEntity> keyPairPermissions = accountService.getAllKeypairPermissions(apiKey);
+
+ List<String> 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<RolePermissionEntity> 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<String> apiNames) {
+ for (APIChecker apiChecker : _apiAccessCheckers) {
+ if (!(apiChecker instanceof ApiRateLimitService)) {
+ continue;
+ }
+ apiChecker.getApisAllowedToUser(role, user, apiNames);
+ return;
+ }
+ }
+
public List<APIChecker> 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 d33774c..59415cc 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 @@
@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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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<ApiDiscoveryResponse> response = (ListResponse<ApiDiscoveryResponse>) discoveryServiceSpy.listApis(getTestUser(), null);
+ ListResponse<ApiDiscoveryResponse> response = (ListResponse<ApiDiscoveryResponse>) 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 917cd7b..afa2b61 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.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 @@
}
@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.",
@@ -208,6 +211,11 @@
}
@Override
+ public List<RolePermissionEntity> getImplicitRolePermissions(RoleType roleType) {
+ return List.of();
+ }
+
+ @Override
public boolean isEnabled() {
if (!enabled) {
logger.debug("API rate limiting is disabled. We will not use ApiRateLimitService.");
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 42e94c1..f945658 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 boolean isUserAllowedToSeeActivationRules(User user) {
- List<ApiDiscoveryResponse> apiList = (List<ApiDiscoveryResponse>) apiDiscoveryService.listApis(user, null).getResponses();
+ List<ApiDiscoveryResponse> apiList = (List<ApiDiscoveryResponse>) 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 1f54804..3629bf2 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 @@
ListResponse<ApiDiscoveryResponse> 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 @@
ListResponse<ApiDiscoveryResponse> 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 @@
ListResponse<ApiDiscoveryResponse> 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 5695325..c05d523 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 @@
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 d19470f..f3ee6bd 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 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 @@
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 d3714f1..7953a6a 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.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 @@
}
@Override
- public String[] createApiKeyAndSecretKey(RegisterUserKeyCmd arg0) {
+ public ApiKeyPair createApiKeyAndSecretKey(RegisterUserKeysCmd arg0) {
// TODO Auto-generated method stub
return null;
}
@@ -401,7 +410,7 @@
}
@Override
- public Pair<User, Account> findUserByApiKey(String arg0) {
+ public Ternary<User, Account, ApiKeyPair> findUserByApiKey(String arg0) {
// TODO Auto-generated method stub
return null;
}
@@ -467,6 +476,10 @@
}
@Override
+ public void validateCallingUserHasAccessToDesiredUser(Long userId) {
+ }
+
+ @Override
public Long finalizeAccountId(String accountName, Long domainId, Long projectId, boolean enabledOnly) {
// TODO Auto-generated method stub
return null;
@@ -503,11 +516,24 @@
}
@Override
- public Pair<Boolean, Map<String, String>> getKeys(Long userId) {
+ public ListResponse<ApiKeyPairResponse> listKeys(ListUserKeysCmd cmd) {
return null;
}
@Override
+ public List<ApiKeyPairPermission> listKeyRules(ListUserKeyRulesCmd cmd) {
+ return null;
+ }
+
+ @Override
+ public void deleteApiKey(DeleteUserKeysCmd cmd) {
+ }
+
+ @Override
+ public void deleteApiKey(ApiKeyPair id) {
+ }
+
+ @Override
public List<UserTwoFactorAuthenticator> listUserTwoFactorAuthenticationProviders() {
return null;
}
@@ -518,6 +544,31 @@
}
@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<RolePermissionEntity> getAllKeypairPermissions(String apiKey) {
+ return List.of();
+ }
+
+ @Override
public void checkAccess(User user, ControlledEntity entity)
throws PermissionDeniedException {
@@ -538,7 +589,7 @@
}
@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 @@
@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 60b5b72..2a29b39 100644
--- a/plugins/network-elements/tungsten/src/main/java/org/apache/cloudstack/network/tungsten/service/TungstenServiceImpl.java
+++ b/plugins/network-elements/tungsten/src/main/java/org/apache/cloudstack/network/tungsten/service/TungstenServiceImpl.java
@@ -19,6 +19,7 @@
import com.cloud.agent.AgentManager;
import com.cloud.agent.api.Answer;
import com.cloud.agent.api.Command;
+import com.cloud.api.ApiDBUtils;
import com.cloud.configuration.Config;
import com.cloud.configuration.ConfigurationManager;
import com.cloud.dc.DataCenter;
@@ -114,6 +115,7 @@
import net.juniper.tungsten.api.types.VirtualMachine;
import net.juniper.tungsten.api.types.VirtualMachineInterface;
import net.juniper.tungsten.api.types.VirtualNetwork;
+import org.apache.cloudstack.acl.ApiKeyPairVO;
import org.apache.cloudstack.api.BaseResponse;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
@@ -1182,9 +1184,10 @@
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 b22d1e7..bad2146 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 @@
when(lbStickinessPolicy.getMethodName()).thenReturn("AppCookie");
List<Pair<String, String>> 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 9b2f3dd..6318c19 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 @@
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 57eeb63..1d6d5d5 100644
--- a/server/src/main/java/com/cloud/api/ApiDBUtils.java
+++ b/server/src/main/java/com/cloud/api/ApiDBUtils.java
@@ -321,7 +321,6 @@
import com.cloud.template.VirtualMachineTemplate;
import com.cloud.user.Account;
import com.cloud.user.AccountDetailsDao;
-import com.cloud.user.AccountManager;
import com.cloud.user.AccountService;
import com.cloud.user.AccountVO;
import com.cloud.user.ResourceLimitService;
@@ -361,6 +360,8 @@
import com.cloud.vm.dao.VMInstanceDao;
import com.cloud.vm.snapshot.VMSnapshot;
import com.cloud.vm.snapshot.dao.VMSnapshotDao;
+import org.apache.cloudstack.acl.ApiKeyPairVO;
+import org.apache.cloudstack.acl.dao.ApiKeyPairDao;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@@ -498,6 +499,7 @@
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 @@
@Inject
private ResourceManagerUtil resourceManagerUtil;
@Inject
+ private ApiKeyPairDao apiKeyPairDao;
+ @Inject
SnapshotPolicyDetailsDao snapshotPolicyDetailsDao;
@Inject
@@ -907,6 +911,7 @@
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 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 @@
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 655f5ac..51cf82b 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 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.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.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 @@
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 @@
consoleSessionResponse.setObjectName("consolesession");
return consoleSessionResponse;
}
+
+ @Override
+ public ApiKeyPairResponse createKeyPairResponse(ApiKeyPair keyPair) {
+ ApiKeyPairResponse apiKeyPairResponse = new ApiKeyPairResponse();
+
+ populateApiKeyPairInApiKeyPairResponse(keyPair, apiKeyPairResponse);
+ populateUserInApiKeyPairResponse(keyPair, apiKeyPairResponse);
+
+ AccountVO account = accountDao.findByIdIncludingRemoved(keyPair.getAccountId());
+ apiKeyPairResponse.setAccountId(account.getUuid());
+ apiKeyPairResponse.setAccountName(account.getAccountName());
+ apiKeyPairResponse.setAccountType(account.getType().toString());
+
+ populateDomainInApiKeyPairResponse(account.getDomainId(), apiKeyPairResponse);
+ populateRoleInApiKeyPairResponse(account.getRoleId(), apiKeyPairResponse);
+
+ return apiKeyPairResponse;
+ }
+
+ protected void populateRoleInApiKeyPairResponse(Long roleId, ApiKeyPairResponse apiKeyPairResponse) {
+ RoleVO roleVO = roleDao.findById(roleId);
+ apiKeyPairResponse.setRoleId(roleVO.getUuid());
+ apiKeyPairResponse.setRoleName(roleVO.getName());
+ apiKeyPairResponse.setRoleType(roleVO.getRoleType().name());
+ }
+
+ protected static void populateApiKeyPairInApiKeyPairResponse(ApiKeyPair keyPair, ApiKeyPairResponse apiKeyPairResponse) {
+ apiKeyPairResponse.setName(keyPair.getName());
+ apiKeyPairResponse.setApiKey(keyPair.getApiKey());
+ apiKeyPairResponse.setSecretKey(keyPair.getSecretKey());
+ apiKeyPairResponse.setDescription(keyPair.getDescription());
+ apiKeyPairResponse.setId(keyPair.getUuid());
+ apiKeyPairResponse.setCreated(keyPair.getCreated());
+ apiKeyPairResponse.setStartDate(keyPair.getStartDate());
+ apiKeyPairResponse.setEndDate(keyPair.getEndDate());
+
+ ApiKeyPairState state = ApiKeyPairState.ENABLED;
+ if (keyPair.getRemoved() != null) {
+ state = ApiKeyPairState.REMOVED;
+ } else if (keyPair.hasEndDatePassed()) {
+ state = ApiKeyPairState.EXPIRED;
+ }
+ apiKeyPairResponse.setState(state);
+ }
+
+ protected void populateUserInApiKeyPairResponse(ApiKeyPair keyPair, ApiKeyPairResponse apiKeyPairResponse) {
+ User user = ApiDBUtils.findUserById(keyPair.getUserId());
+ apiKeyPairResponse.setUserId(user.getUuid());
+ apiKeyPairResponse.setUsername(user.getUsername());
+ }
+
+ protected void populateDomainInApiKeyPairResponse(Long domainId, ApiKeyPairResponse apiKeyPairResponse) {
+ DomainVO domainVO = domainDao.findById(domainId);
+ apiKeyPairResponse.setDomainId(domainVO.getUuid());
+ apiKeyPairResponse.setDomainName(domainVO.getName());
+ StringBuilder domainPath = new StringBuilder("ROOT");
+ (domainPath.append(domainVO.getPath())).deleteCharAt(domainPath.length() - 1);
+ apiKeyPairResponse.setDomainPath(domainPath.toString());
+ }
+
+ @Override
+ public ListResponse<BaseRolePermissionResponse> createKeypairPermissionsResponse(final List<ApiKeyPairPermission> permissions) {
+ final ListResponse<BaseRolePermissionResponse> response = new ListResponse<>();
+ final List<BaseRolePermissionResponse> 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 8332578..dc07814 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 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.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 @@
private UUIDManager uuidMgr;
@Inject
private UserPasswordResetManager userPasswordResetManager;
+ @Inject
+ private ApiKeyPairManagerImpl keyPairManager;
private List<PluggableService> pluggableServices;
@@ -1081,14 +1087,16 @@
txn.close();
User user;
// verify there is a user with this api key
- final Pair<User, Account> userAcctPair = accountMgr.findUserByApiKey(apiKey);
- if (userAcctPair == null) {
+ final Ternary<User, Account, ApiKeyPair> 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 @@
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 @@
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;
}
- return equalSig;
+ CallContext.register(user, account);
+
+ List<ApiKeyPairPermission> 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;
+ }
} 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 @@
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 @@
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 dd99b4e..bfeb3d2 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 @@
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 @@
// 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 0cec3a3..fb97e2f 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 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.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.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.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;
@@ -378,6 +383,9 @@
AccountManager accountMgr;
@Inject
+ RoleService roleService;
+
+ @Inject
ProjectManager _projectMgr;
@Inject
@@ -646,6 +654,9 @@
@Inject
ExtensionHelper extensionHelper;
+ @Inject
+ RoleDao roleDao;
+
/*
* (non-Javadoc)
*
@@ -826,6 +837,37 @@
}
@Override
+ public List<Long> searchForAccessibleUsers() {
+ List<Long> permittedAccounts = new ArrayList<>();
+ Account callingAccount = CallContext.current().getCallingAccount();
+ Filter searchFilter = new Filter(UserAccountJoinVO.class, "id", true);
+ List<RoleVO> allowedRoles = roleDao.listAll();
+ roleService.removeRolesIfNeeded(allowedRoles);
+ List<Long> allowedRolesId = allowedRoles.stream().map(RoleVO::getId).collect(Collectors.toList());
+
+ Pair<List<UserAccountJoinVO>, 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<EventResponse> searchForEvents(ListEventsCmd cmd) {
Pair<List<EventJoinVO>, Integer> result = searchForEventsInternal(cmd);
ListResponse<EventResponse> response = new ListResponse<>();
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 cff758d..e0d88fb 100644
--- a/server/src/main/java/com/cloud/api/query/dao/UserAccountJoinDao.java
+++ b/server/src/main/java/com/cloud/api/query/dao/UserAccountJoinDao.java
@@ -19,6 +19,7 @@
import java.util.List;
import org.apache.cloudstack.api.ResponseObject;
+import org.apache.cloudstack.acl.ApiKeyPairVO;
import org.apache.cloudstack.api.response.UserResponse;
import com.cloud.api.query.vo.UserAccountJoinVO;
@@ -28,7 +29,7 @@
public interface UserAccountJoinDao extends GenericDao<UserAccountJoinVO, Long> {
- 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 f2c234b..f7dddb3 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 @@
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 @@
}
@Override
- public UserResponse newUserResponse(ResponseView view, UserAccountJoinVO usr) {
+ public UserResponse newUserResponse(ResponseView view, UserAccountJoinVO usr, ApiKeyPairVO lastKeyPair) {
UserResponse userResponse = new UserResponse();
userResponse.setAccountId(usr.getAccountUuid());
userResponse.setAccountName(usr.getAccountName());
@@ -69,10 +71,14 @@
userResponse.setState(usr.getState().toString());
userResponse.setTimezone(usr.getTimezone());
userResponse.setUsername(usr.getUsername());
- userResponse.setApiKey(usr.getApiKey());
- userResponse.setSecretKey(usr.getSecretKey());
userResponse.setIsDefault(usr.isDefault());
userResponse.set2FAenabled(usr.isUser2faEnabled());
+ if (lastKeyPair != null) {
+ userResponse.setApiKey(lastKeyPair.getApiKey());
+ if (AccountManager.UseSecretKeyInResponse.value()) {
+ userResponse.setSecretKey(lastKeyPair.getSecretKey());
+ }
+ }
long domainId = usr.getDomainId();
boolean is2FAmandated = Boolean.TRUE.equals(AccountManagerImpl.enableUserTwoFactorAuthentication.valueIn(domainId)) && Boolean.TRUE.equals(AccountManagerImpl.mandateUserTwoFactorAuthentication.valueIn(domainId));
userResponse.set2FAmandated(is2FAmandated);
diff --git a/server/src/main/java/com/cloud/api/query/vo/UserAccountJoinVO.java b/server/src/main/java/com/cloud/api/query/vo/UserAccountJoinVO.java
index ad005ee..774759d 100644
--- a/server/src/main/java/com/cloud/api/query/vo/UserAccountJoinVO.java
+++ b/server/src/main/java/com/cloud/api/query/vo/UserAccountJoinVO.java
@@ -19,7 +19,6 @@
import com.cloud.user.Account;
import com.cloud.user.User;
import com.cloud.user.UserAccount;
-import com.cloud.utils.db.Encrypt;
import com.cloud.utils.db.GenericDao;
import org.apache.cloudstack.api.Identity;
import org.apache.cloudstack.api.InternalIdentity;
@@ -61,13 +60,6 @@
@Column(name = "state")
private String state;
- @Column(name = "api_key")
- private String apiKey = null;
-
- @Encrypt
- @Column(name = "secret_key")
- private String secretKey = null;
-
@Column(name = GenericDao.CREATED_COLUMN)
private Date created;
@@ -209,14 +201,6 @@
return state;
}
- public String getApiKey() {
- return apiKey;
- }
-
- public String getSecretKey() {
- return secretKey;
- }
-
public Date getCreated() {
return created;
}
diff --git a/server/src/main/java/com/cloud/network/as/AutoScaleManagerImpl.java b/server/src/main/java/com/cloud/network/as/AutoScaleManagerImpl.java
index 4e07611..805a897 100644
--- a/server/src/main/java/com/cloud/network/as/AutoScaleManagerImpl.java
+++ b/server/src/main/java/com/cloud/network/as/AutoScaleManagerImpl.java
@@ -38,6 +38,8 @@
import javax.inject.Inject;
+import com.cloud.network.NetworkModel;
+import org.apache.cloudstack.acl.ApiKeyPairVO;
import org.apache.cloudstack.acl.ControlledEntity;
import org.apache.cloudstack.affinity.AffinityGroupVO;
import org.apache.cloudstack.affinity.dao.AffinityGroupDao;
@@ -112,7 +114,6 @@
import com.cloud.network.Network;
import com.cloud.network.Network.Capability;
import com.cloud.network.Network.IpAddresses;
-import com.cloud.network.NetworkModel;
import com.cloud.network.as.AutoScaleCounter.AutoScaleCounterParam;
import com.cloud.network.as.dao.AutoScalePolicyConditionMapDao;
import com.cloud.network.as.dao.AutoScalePolicyDao;
@@ -518,15 +519,10 @@
throw new InvalidParameterValueException("AutoScale User id does not belong to the same account");
}
- String apiKey = user.getApiKey();
- String secretKey = user.getSecretKey();
+ ApiKeyPairVO latestKeypair = ApiDBUtils.searchForLatestUserKeyPair(user.getId());
- if (apiKey == null) {
- throw new InvalidParameterValueException("apiKey for user: " + user.getUsername() + " is empty. Please generate it");
- }
-
- if (secretKey == null) {
- throw new InvalidParameterValueException("secretKey for user: " + user.getUsername() + " is empty. Please generate it");
+ if (latestKeypair == null) {
+ throw new InvalidParameterValueException(String.format("No API keypair for user [%s]. Please generate it.", user.getUsername()));
}
ApiServiceConfiguration.validateEndpointUrl();
diff --git a/server/src/main/java/com/cloud/network/lb/LoadBalancingRulesManagerImpl.java b/server/src/main/java/com/cloud/network/lb/LoadBalancingRulesManagerImpl.java
index ffa22e2..ced1d78 100644
--- a/server/src/main/java/com/cloud/network/lb/LoadBalancingRulesManagerImpl.java
+++ b/server/src/main/java/com/cloud/network/lb/LoadBalancingRulesManagerImpl.java
@@ -16,6 +16,7 @@
// under the License.
package com.cloud.network.lb;
+import com.cloud.api.ApiDBUtils;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@@ -31,6 +32,7 @@
import com.cloud.offerings.NetworkOfferingServiceMapVO;
import com.cloud.offerings.dao.NetworkOfferingServiceMapDao;
+import org.apache.cloudstack.acl.ApiKeyPairVO;
import org.apache.cloudstack.acl.SecurityChecker;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.ApiErrorCode;
@@ -347,15 +349,15 @@
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 e579eee..19a5896 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 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 @@
if (user == null) {
logger.warn("global setting[baremetal.provision.done.notification] is enabled but user baremetal-system-account is not found. Baremetal provision done notification will not be enabled");
} else {
- buf.append(String.format(" baremetalnotificationsecuritykey=%s", user.getSecretKey()));
- buf.append(String.format(" baremetalnotificationapikey=%s", user.getApiKey()));
+ ApiKeyPairVO latestKeypair = ApiDBUtils.searchForLatestUserKeyPair(user.getId());
+
+ if (latestKeypair == null) {
+ throw new InvalidParameterValueException(String.format("No API keypair for user [%s]. Please generate it.", user.getUsername()));
+ }
+
+ buf.append(String.format(" baremetalnotificationsecuritykey=%s", latestKeypair.getSecretKey()));
+ buf.append(String.format(" baremetalnotificationapikey=%s", latestKeypair.getApiKey()));
buf.append(" host=").append(ApiServiceConfiguration.ManagementServerAddresses.value());
buf.append(" port=").append(_configDao.getValue(Config.BaremetalProvisionDoneNotificationPort.key()));
}
diff --git a/server/src/main/java/com/cloud/server/ManagementServerImpl.java b/server/src/main/java/com/cloud/server/ManagementServerImpl.java
index b595d01..c68d9ea 100644
--- a/server/src/main/java/com/cloud/server/ManagementServerImpl.java
+++ b/server/src/main/java/com/cloud/server/ManagementServerImpl.java
@@ -43,6 +43,7 @@
import javax.inject.Inject;
import javax.naming.ConfigurationException;
+import org.apache.cloudstack.acl.ApiKeyPairVO;
import com.cloud.network.vpc.VpcVO;
import org.apache.cloudstack.acl.ControlledEntity;
import org.apache.cloudstack.acl.SecurityChecker;
@@ -275,15 +276,18 @@
import org.apache.cloudstack.api.command.admin.usage.RemoveRawUsageRecordsCmd;
import org.apache.cloudstack.api.command.admin.usage.UpdateTrafficTypeCmd;
import org.apache.cloudstack.api.command.admin.user.CreateUserCmd;
+import org.apache.cloudstack.api.command.admin.user.DeleteUserKeysCmd;
import org.apache.cloudstack.api.command.admin.user.DeleteUserCmd;
import org.apache.cloudstack.api.command.admin.user.DisableUserCmd;
import org.apache.cloudstack.api.command.admin.user.EnableUserCmd;
import org.apache.cloudstack.api.command.admin.user.GetUserCmd;
import org.apache.cloudstack.api.command.admin.user.GetUserKeysCmd;
+import org.apache.cloudstack.api.command.admin.user.ListUserKeyRulesCmd;
+import org.apache.cloudstack.api.command.admin.user.ListUserKeysCmd;
import org.apache.cloudstack.api.command.admin.user.ListUsersCmd;
import org.apache.cloudstack.api.command.admin.user.LockUserCmd;
import org.apache.cloudstack.api.command.admin.user.MoveUserCmd;
-import org.apache.cloudstack.api.command.admin.user.RegisterUserKeyCmd;
+import org.apache.cloudstack.api.command.admin.user.RegisterUserKeysCmd;
import org.apache.cloudstack.api.command.admin.user.UpdateUserCmd;
import org.apache.cloudstack.api.command.admin.vlan.CreateVlanIpRangeCmd;
import org.apache.cloudstack.api.command.admin.vlan.DedicatePublicIpRangeCmd;
@@ -3947,7 +3951,7 @@
cmdList.add(ListUsersCmd.class);
cmdList.add(LockUserCmd.class);
cmdList.add(MoveUserCmd.class);
- cmdList.add(RegisterUserKeyCmd.class);
+ cmdList.add(RegisterUserKeysCmd.class);
cmdList.add(UpdateUserCmd.class);
cmdList.add(CreateVlanIpRangeCmd.class);
cmdList.add(UpdateVlanIpRangeCmd.class);
@@ -4375,6 +4379,10 @@
cmdList.add(CreateConsoleEndpointCmd.class);
cmdList.add(ListConsoleSessionsCmd.class);
+ cmdList.add(DeleteUserKeysCmd.class);
+ cmdList.add(ListUserKeysCmd.class);
+ cmdList.add(ListUserKeyRulesCmd.class);
+
//user data APIs
cmdList.add(RegisterUserDataCmd.class);
cmdList.add(DeleteUserDataCmd.class);
@@ -4777,7 +4785,13 @@
try {
// get the user obj to get their secret key
user = _accountMgr.getActiveUser(userId);
- final String secretKey = user.getSecretKey();
+ ApiKeyPairVO latestKeypair = ApiDBUtils.searchForLatestUserKeyPair(user.getId());
+
+ if (latestKeypair == null) {
+ throw new InvalidParameterValueException(String.format("No API keypair for user [%s]. Please generate it.", user.getUsername()));
+ }
+
+ final String secretKey = latestKeypair.getSecretKey();
signature = signRequest(cloudIdentifier, secretKey);
} catch (final Exception e) {
logger.warn("Exception whilst creating a signature:" + e);
diff --git a/server/src/main/java/com/cloud/servlet/ConsoleProxyServlet.java b/server/src/main/java/com/cloud/servlet/ConsoleProxyServlet.java
index 265a975..6886e0e 100644
--- a/server/src/main/java/com/cloud/servlet/ConsoleProxyServlet.java
+++ b/server/src/main/java/com/cloud/servlet/ConsoleProxyServlet.java
@@ -35,6 +35,7 @@
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
+import org.apache.cloudstack.acl.apikeypair.ApiKeyPair;
import org.apache.cloudstack.framework.security.keys.KeysManager;
import org.apache.commons.codec.binary.Base64;
import org.apache.logging.log4j.Logger;
@@ -543,24 +544,23 @@
txn.close();
User user = null;
// verify there is a user with this api key
- Pair<User, Account> userAcctPair = _accountMgr.findUserByApiKey(apiKey);
- if (userAcctPair == null) {
+ Ternary<User, Account, ApiKeyPair> 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 c46ac78..98d2419 100644
--- a/server/src/main/java/com/cloud/user/AccountManager.java
+++ b/server/src/main/java/com/cloud/user/AccountManager.java
@@ -21,6 +21,7 @@
import java.util.Map;
import org.apache.cloudstack.acl.ControlledEntity;
+import org.apache.cloudstack.acl.apikeypair.ApiKeyPair;
import org.apache.cloudstack.api.command.admin.account.UpdateAccountCmd;
import org.apache.cloudstack.api.command.admin.user.DeleteUserCmd;
import org.apache.cloudstack.api.command.admin.user.MoveUserCmd;
@@ -35,7 +36,6 @@
import com.cloud.exception.ConcurrentOperationException;
import com.cloud.exception.ResourceUnavailableException;
import com.cloud.projects.Project.ListProjectResourcesCriteria;
-import com.cloud.utils.Pair;
import com.cloud.utils.Ternary;
import com.cloud.utils.db.SearchBuilder;
import com.cloud.utils.db.SearchCriteria;
@@ -85,7 +85,7 @@
* that was created for a particular user
* @return the user/account pair if one exact match was found, null otherwise
*/
- Pair<User, Account> findUserByApiKey(String apiKey);
+ Ternary<User, Account, ApiKeyPair> findUserByApiKey(String apiKey);
boolean enableAccount(long accountId);
@@ -202,9 +202,13 @@
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 09ef9fe..d9af7af 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.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.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.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.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.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.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 @@
@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 @@
@Inject
private VolumeApiService volumeService;
@Inject
+ private QueryService queryService;
+ @Inject
private AffinityGroupDao _affinityGroupDao;
@Inject
private AccountGuestVlanMapDao _accountGuestVlanMapDao;
@@ -398,6 +430,8 @@
true,
ConfigKey.Scope.Domain);
+ private Map<RoleType, Set<String>> annotationRoleBasedApisMap = new HashMap<>();
+
static ConfigKey<Boolean> userAllowMultipleAccounts = new ConfigKey<>("Advanced",
Boolean.class,
"user.allow.multiple.accounts",
@@ -424,6 +458,9 @@
protected AccountManagerImpl() {
super();
+ for (RoleType roleType : RoleType.values()) {
+ annotationRoleBasedApisMap.put(roleType, new HashSet<>());
+ }
}
public List<UserAuthenticator> getUserAuthenticators() {
@@ -903,6 +940,11 @@
return cleanupAccount(account, callerUserId, caller);
}
+ protected void removeUserApiKeys(Long userId) {
+ List<ApiKeyPairVO> apiKeyPairs = apiKeyPairDao.listApiKeysByUserOrApiKeyId(userId, null).first();
+ apiKeyPairs.forEach(keyPair -> _accountService.deleteApiKey(keyPair));
+ }
+
protected void cleanupPluginsResourcesIfNeeded(Account account) {
try {
KubernetesServiceHelper kubernetesServiceHelper =
@@ -921,6 +963,7 @@
// cleanup the users from the account
List<UserVO> 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 @@
}
}
- private void checkApiAccess(List<APIChecker> apiCheckers, Account caller, String command) {
+ private void checkApiAccess(List<APIChecker> 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<APIChecker> apiCheckers = getEnabledApiCheckers();
- checkApiAccess(apiCheckers, caller, command);
+
+ List<ApiKeyPairPermission> keyPairPermissions = new ArrayList<>();
+ if (apiKey != null) {
+ Ternary<User, Account, ApiKeyPair> 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 @@
}
}
- 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 @@
}
}
- 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 @@
}
/**
- * 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.
* <ul>
* <li>When updating the keys, it must be provided a pair (API and Secret keys); otherwise, an {@link InvalidParameterValueException} is thrown.
* <li>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 @@
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<User, Account, ApiKeyPair> 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 @@
// 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 @@
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 @@
}
@Override
- public Pair<User, Account> 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<User, Account, ApiKeyPair> 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<Boolean, Map<String, String>> getKeys(GetUserKeysCmd cmd) {
- final long userId = cmd.getID();
- return getKeys(userId);
- }
-
- @Override
- public Pair<Boolean, Map<String, String>> 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<String, String> 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 @@
return new Pair<>(apiKeyAccess, keys);
}
+ @Override
+ public ListResponse<ApiKeyPairResponse> listKeys(ListUserKeysCmd cmd) {
+ ListResponse<ApiKeyPairResponse> finalResponse = new ListResponse<>();
+ List<ApiKeyPairResponse> 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<ApiKeyPairResponse> 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<ApiKeyPairResponse> responses, ListUserKeysCmd cmd) {
+ List<Long> 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<List<ApiKeyPairVO>, 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<ApiKeyPairPermission> 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<RolePermissionEntity> baseKeyPairPermissions, List<RolePermissionEntity> comparedPermissions) {
+ Map<String, RolePermissionEntity> 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<ApiKeyPairPermissionVO> permissions = apiKeyPairPermissionsDao.findAllByApiKeyPairId(keyPair.getId());
+ for (ApiKeyPairPermission permission : permissions) {
+ apiKeyPairPermissionsDao.remove(permission.getId());
+ }
+ apiKeyPairDao.remove(keyPair.getId());
+ }
+
+ private void addKeypairResponse(ApiKeyPair keyPair, List<ApiKeyPairResponse> 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<ApiKeyPairPermission> 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());
@@ -3151,6 +3421,11 @@
}
@Override
+ public ApiKeyPair getKeyPairByApiKey(String apiKey) {
+ return apiKeyPairDao.findByApiKey(apiKey);
+ }
+
+ @Override
public List<UserTwoFactorAuthenticator> listUserTwoFactorAuthenticationProviders() {
return userTwoFactorAuthenticationProviders;
}
@@ -3174,39 +3449,43 @@
@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<Map<String, Object>> 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<ApiKeyPairVO>) status -> {
+ createUserApiKey(userId, newApiKeyPair);
+ createUserSecretKey(userId, newApiKeyPair);
+ return validateAndPersistKeyPairAndPermissions(account, newApiKeyPair, rules, cmd);
});
-
- return keys;
}
@Override
@@ -3221,37 +3500,91 @@
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<Map<String, Object>> rules, RegisterUserKeysCmd cmd) {
+ String accessingApiKey = getAccessingApiKey(cmd);
+ final Role accountRole = roleService.findRole(account.getRoleId());
+ List<RolePermissionEntity> allPermissions = accessingApiKey == null ?
+ roleService.findAllRolePermissionsEntityBy(accountRole.getId(), true) : getAllKeypairPermissions(accessingApiKey);
+ List<RolePermissionEntity> permissions = new ArrayList<>();
+ for (Map<String, Object> 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<RolePermissionEntity> 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<ApiKeyPairPermission> 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 @@
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 @@
return null;
}
+ public ApiKeyPair getLatestUserKeyPair(Long userId) {
+ return ApiDBUtils.searchForLatestUserKeyPair(userId);
+ }
+
@Override
public void buildACLSearchBuilder(SearchBuilder<? extends ControlledEntity> sb, Long domainId, boolean isRecursive, List<Long> permittedAccounts,
ListProjectResourcesCriteria listProjectResourcesCriteria) {
@@ -3492,7 +3827,8 @@
@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 @@
@Override
public UserAccount getUserAccountById(Long userId) {
UserAccount userAccount = userAccountDao.findById(userId);
- Map<String, String> details = _userDetailsDao.listDetailsKeyPairs(userId);
- userAccount.setDetails(details);
-
+ if (userAccount != null) {
+ Map<String, String> 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 36f1f7a..4597ef8 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.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.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 @@
protected static long ROOT_DEVICE_ID = 0;
+ private final Type jobParamsType = new TypeToken<HashMap<String, String>>() {}.getType();
+
public List<KubernetesServiceHelper> getKubernetesServiceHelpers() {
return kubernetesServiceHelpers;
}
@@ -3459,14 +3463,14 @@
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 @@
boolean expunge = cmd.getExpunge();
if (expunge) {
- checkExpungeVmPermission(ctx.getCallingAccount());
+ String jobParamsString = ((AsyncJobVO) cmd.getJob()).getCmdInfo();
+ HashMap<String,String> 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 0000000..07ad383
--- /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<ApiKeyPairPermission> findAllPermissionsByKeyPairId(Long apiKeyPairId, Long roleId) {
+ List<ApiKeyPairPermissionVO> keyPairPermissions = apiKeyPairPermissionsDao.findAllByKeyPairIdSorted(apiKeyPairId);
+ List<RolePermissionEntity> 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<String, RolePermissionEntity> rolePermissionInfo = roleService.getRoleRulesAndPermissions(rolePermissions);
+ if (roleService.roleHasPermission(rolePermissionInfo, new ArrayList<>(keyPairPermissions))) {
+ return new ArrayList<>(keyPairPermissions);
+ }
+
+ Map<String, RolePermissionEntity> 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<String, RolePermissionEntity> getRulesToBeKeptForTheKeyPair(Map<String, RolePermissionEntity> rolePermissions, Map<String, RolePermissionEntity> keyPairPermissions) {
+ Map<String, RolePermissionEntity> rulesToBeKept = new HashMap<>();
+ for (Map.Entry<String, RolePermissionEntity> 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 d1ae1b4..0cf874d 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 @@
private RolePermissionsDao rolePermissionsDao;
@Inject
private AccountManager accountManager;
+ private List<APIChecker> apiAccessCheckers;
public void checkCallerAccess() {
if (!isEnabled()) {
@@ -449,20 +450,21 @@
/**
* 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<? extends Role> roles) {
+ @Override
+ public int removeRolesIfNeeded(List<? extends Role> roles) {
if (roles.isEmpty()) {
return 0;
}
Long callerRoleId = getCurrentAccount().getRoleId();
- Map<String, Permission> callerRolePermissions = getRoleRulesAndPermissions(callerRoleId);
+ Map<String, RolePermissionEntity> callerRolePermissions = getRoleRulesAndPermissions(findAllRolePermissionsEntityBy(callerRoleId, false));
int count = 0;
Iterator<? extends Role> 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 @@
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<String, Permission> sourceRolePermissions, Role targetRole) {
+
+ @Override
+ public boolean roleHasPermission(Map<String, RolePermissionEntity> rolePermissions, List<RolePermissionEntity> rolePermissionsToAccess) {
Set<String> 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 @@
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 @@
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<String, Permission> getRoleRulesAndPermissions(Long roleId) {
- Map<String, Permission> roleRulesAndPermissions = new HashMap<>();
+ @Override
+ public Map<String, RolePermissionEntity> getRoleRulesAndPermissions(List<RolePermissionEntity> rolePermissions) {
+ Map<String, RolePermissionEntity> 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<String, RolePermissionEntity> 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<Role> findRolesByType(RoleType roleType) {
return findRolesByType(roleType, null, null, null).first();
@@ -571,6 +567,43 @@
return Collections.emptyList();
}
+ @Override
+ public List<RolePermissionEntity> findAllRolePermissionsEntityBy(final Long roleId, final boolean considerImplicitRules) {
+ List<RolePermissionVO> rolePermissions = rolePermissionsDao.findAllByRoleIdSorted(roleId);
+ if (!considerImplicitRules) {
+ return new ArrayList<>(rolePermissions);
+ }
+
+ List<RolePermissionEntity> permissions = new ArrayList<>();
+ List<RolePermissionEntity> 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<RolePermissionEntity> getImplicitRolePermissions(Long roleId) {
+ Role role = roleDao.findById(roleId);
+ if (role == null) {
+ return List.of();
+ }
+
+ for (APIChecker apiChecker : apiAccessCheckers) {
+ List<RolePermissionEntity> 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 @@
cmdList.add(DisableRoleCmd.class);
return cmdList;
}
+
+ @Inject
+ public void setApiAccessCheckers(List<APIChecker> 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 b90c40d..37d32c0 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}" />
</bean>
- <bean id="roleManagerImpl" class="org.apache.cloudstack.acl.RoleManagerImpl" />
+ <bean id="roleManagerImpl" class="org.apache.cloudstack.acl.RoleManagerImpl">
+ <property name="apiAccessCheckers" value="#{apiAclCheckersRegistry.registered}" />
+ </bean>
<bean id="projRoleManagerImpl" class="org.apache.cloudstack.acl.ProjectRoleManagerImpl" />
+ <bean id="ApiKeyPairManagerImpl" class="org.apache.cloudstack.acl.ApiKeyPairManagerImpl" />
+
<bean id="accountManagerImpl" class="com.cloud.user.AccountManagerImpl">
<property name="userAuthenticators"
value="#{userAuthenticatorsRegistry.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 c186083..1b864bd 100644
--- a/server/src/test/java/com/cloud/network/as/AutoScaleManagerImplTest.java
+++ b/server/src/test/java/com/cloud/network/as/AutoScaleManagerImplTest.java
@@ -76,6 +76,9 @@
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.test.util.ReflectionTestUtils;
+import com.cloud.api.ApiDBUtils;
+import org.apache.cloudstack.acl.ApiKeyPairVO;
+import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService;
import com.cloud.agent.AgentManager;
import com.cloud.agent.api.PerformanceMonitorAnswer;
import com.cloud.agent.api.PerformanceMonitorCommand;
@@ -266,9 +269,14 @@
@Mock
GuestOSDao guestOSDao;
+ @Mock
+ NetworkOrchestrationService networkOrchestrationService;
+
AccountVO account;
UserVO user;
+ MockedStatic<ApiDBUtils> mockedApiDBUtils;
+
final static String INVALID = "invalid";
private static final Long counterId = 1L;
@@ -419,6 +427,11 @@
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 @@
@After
public void tearDown() {
+ mockedApiDBUtils.close();
CallContext.unregister();
}
@@ -879,9 +893,6 @@
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");
@@ -892,43 +903,22 @@
@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);
autoScaleManagerImplSpy.checkAutoScaleUser(autoScaleUserId, accountId);
}
@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 e5d09ba..c476895 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 @@
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.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 @@
@Mock
private AccountService accountService;
@Mock
- private GetUserKeysCmd _listkeyscmd;
+ private GetUserKeysCmd _getkeyscmd;
+ @Mock
+ private ListUserKeysCmd listUserKeysCmd;
@Mock
private User _user;
@Mock
@@ -102,6 +121,15 @@
@Mock
private UserVO userVoMock;
+ @Mock
+ private ApiKeyPairService apiKeyPairService;
+
+ @Mock
+ private ApiKeyPairVO apiKeyPairVOMock;
+
+ @Mock
+ private Pair<List<ApiKeyPairVO>, Integer> pairMock;
+
private final long accountMockId = 100L;
@Mock
@@ -128,9 +156,19 @@
@Mock
ConfigKey<Boolean> 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 @@
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 @@
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 @@
}
@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 @@
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 @@
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 @@
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 @@
}
@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<User, Account, ApiKeyPair> pairUserAccountMock = new Ternary<User, Account, ApiKeyPair>(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<ActionEventUtils> eventUtils = Mockito.mockStatic(ActionEventUtils.class)) {
Mockito.when(ActionEventUtils.onActionEvent(Mockito.anyLong(), Mockito.anyLong(),
@@ -599,7 +649,7 @@
}
@Test
- public void validateAndUpdatAccountApiKeyAccess() {
+ public void validateAndUpdateAccountApiKeyAccess() {
Mockito.doReturn("Inherit").when(UpdateAccountCmdMock).getApiKeyAccess();
try (MockedStatic<ActionEventUtils> eventUtils = Mockito.mockStatic(ActionEventUtils.class)) {
Mockito.when(ActionEventUtils.onActionEvent(Mockito.anyLong(), Mockito.anyLong(),
@@ -613,7 +663,7 @@
}
@Test(expected = InvalidParameterValueException.class)
- public void validateAndUpdatAccountApiKeyAccessInvalidParameter() {
+ public void validateAndUpdateAccountApiKeyAccessInvalidParameter() {
Mockito.doReturn("False").when(UpdateAccountCmdMock).getApiKeyAccess();
accountManagerImpl.validateAndUpdateAccountApiKeyAccess(UpdateAccountCmdMock, accountVoMock);
}
@@ -1272,6 +1322,354 @@
}
@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<Map<String, Object>> 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<Map<String, Object>> 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<Map<String, Object>> 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<Map<String, Object>> 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<Map<String, Object>> 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<Map<String, Object>> 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<Map<String, Object>> 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<Map<String, Object>> 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<ComponentContext> mockedComponentContext = Mockito.mockStatic(ComponentContext.class)) {
WebhookHelper webhookHelper = Mockito.mock(WebhookHelper.class);
@@ -1555,6 +1953,15 @@
}
@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);
Mockito.lenient().when(accountManagerImpl.getRoleType(Mockito.eq(accountMock))).thenReturn(RoleType.DomainAdmin);
diff --git a/server/src/test/java/com/cloud/vm/UserVmManagerImplTest.java b/server/src/test/java/com/cloud/vm/UserVmManagerImplTest.java
index 4edafb3..f743938 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.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 @@
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 @@
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<CallContext> mockedCallContext = mockStatic(CallContext.class)) {
mockedCallContext.when(CallContext::current).thenReturn(callContext);
mockedCallContext.when(() -> CallContext.register(callContext, ApiCommandResourceType.Volume)).thenReturn(callContext);
@@ -3671,6 +3672,9 @@
when(cmd.getExpunge()).thenReturn(expunge);
List<Long> 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 5d9ee26..7b070d4 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.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 @@
@Mock
private RolePermission rolePermission2Mock;
@Mock
+ private RolePermission rolePermission2MockWithDeniedPermission;
+ @Mock
+ private RolePermission rolePermission3Mock;
+ @Mock
private Account callerAccountMock;
@Mock
private Role callerAccountRoleMock;
@@ -76,7 +79,7 @@
private RoleVO roleVoMock;
private long roleMockId = 1l;
- private Map<String, Permission> rolePermissions = new HashMap<>();
+ private Map<String, RolePermissionEntity> rolePermissions = new HashMap<>();
public void setUpRoleVisibilityTests() {
Mockito.doReturn(List.of("api1", "api2", "api3")).when(accountManagerMock).getApiNameList();
@@ -89,18 +92,11 @@
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<RolePermission> lessPermissionsRolePermissions = Collections.singletonList(rolePermission1Mock);
Mockito.doReturn(1L).when(lessPermissionsRoleMock).getId();
- Mockito.when(roleManagerImpl.findAllPermissionsBy(1L)).thenReturn(lessPermissionsRolePermissions);
-
- List<RolePermission> morePermissionsRolePermissions = List.of(rolePermission1Mock, rolePermission2Mock);
Mockito.doReturn(2L).when(morePermissionsRoleMock).getId();
- Mockito.when(roleManagerImpl.findAllPermissionsBy(morePermissionsRoleMock.getId())).thenReturn(morePermissionsRolePermissions);
-
- List<RolePermission> differentPermissionsRolePermissions = Collections.singletonList(rolePermission2Mock);
Mockito.doReturn(3L).when(differentPermissionsRoleMock).getId();
- Mockito.when(roleManagerImpl.findAllPermissionsBy(differentPermissionsRoleMock.getId())).thenReturn(differentPermissionsRolePermissions);
}
@Before
@@ -225,9 +221,6 @@
setUpRoleVisibilityTests();
List<Role> roles = new ArrayList<>();
- List<RolePermission> callerAccountRolePermissions = List.of(rolePermission1Mock, rolePermission2Mock);
- Mockito.when(roleManagerImpl.findAllPermissionsBy(callerAccountRoleMock.getId())).thenReturn(callerAccountRolePermissions);
-
roles.add(callerAccountRoleMock);
roles.add(lessPermissionsRoleMock);
@@ -243,9 +236,11 @@
setUpRoleVisibilityTests();
List<Role> roles = new ArrayList<>();
- List<RolePermission> callerAccountRolePermissions = Collections.singletonList(rolePermission1Mock);
- Mockito.when(roleManagerImpl.findAllPermissionsBy(callerAccountRoleMock.getId())).thenReturn(callerAccountRolePermissions);
+ List<RolePermissionEntity> callerAccountRolePermissions = Collections.singletonList(rolePermission1Mock);
+ Mockito.when(roleManagerImpl.findAllRolePermissionsEntityBy(callerAccountRoleMock.getId(), false)).thenReturn(callerAccountRolePermissions);
+ List<RolePermissionEntity> callerAccountRolePermissions2 = Collections.singletonList(rolePermission2Mock);
+ Mockito.when(roleManagerImpl.findAllRolePermissionsEntityBy(morePermissionsRoleMock.getId(), false)).thenReturn(callerAccountRolePermissions2);
roles.add(callerAccountRoleMock);
roles.add(morePermissionsRoleMock);
@@ -261,8 +256,11 @@
setUpRoleVisibilityTests();
List<Role> roles = new ArrayList<>();
- List<RolePermission> callerAccountRolePermissions = Collections.singletonList(rolePermission1Mock);
- Mockito.when(roleManagerImpl.findAllPermissionsBy(callerAccountRoleMock.getId())).thenReturn(callerAccountRolePermissions);
+ List<RolePermissionEntity> callerAccountRolePermissions = Collections.singletonList(rolePermission1Mock);
+ Mockito.when(roleManagerImpl.findAllRolePermissionsEntityBy(callerAccountRoleMock.getId(), false)).thenReturn(callerAccountRolePermissions);
+
+ List<RolePermissionEntity> callerAccountRolePermissions3 = Collections.singletonList(rolePermission2Mock);
+ Mockito.when(roleManagerImpl.findAllRolePermissionsEntityBy(differentPermissionsRoleMock.getId(), false)).thenReturn(callerAccountRolePermissions3);
roles.add(callerAccountRoleMock);
roles.add(differentPermissionsRoleMock);
@@ -276,10 +274,10 @@
@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 @@
@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 @@
@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 void getRolePermissionsTestRoleReturnsRolePermissions() {
setUpRoleVisibilityTests();
- Map<String, Permission> roleRulesAndPermissions = roleManagerImpl.getRoleRulesAndPermissions(morePermissionsRoleMock.getId());
+ Map<String, RolePermissionEntity> 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 209c843..d30f9f3 100644
--- a/test/integration/smoke/test_accounts.py
+++ b/test/integration/smoke/test_accounts.py
@@ -1744,7 +1744,7 @@
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 8c5a0d6..fb2afc1 100644
--- a/tools/marvin/marvin/cloudstackTestClient.py
+++ b/tools/marvin/marvin/cloudstackTestClient.py
@@ -148,32 +148,24 @@
"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 @@
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 @@
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 2405d54..825159a 100755
--- a/tools/marvin/marvin/lib/base.py
+++ b/tools/marvin/marvin/lib/base.py
@@ -335,6 +335,7 @@
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):