Skip to content

Commit 95e0b8d

Browse files
CLOUDSTACK-8562: DB-Backed Dynamic Role Based API Access Checker
This feature allows root administrators to define new roles and associate API permissions to them. A limited form of role-based access control for the CloudStack management server API is provided through a properties file, commands.properties, embedded in the WAR distribution. Therefore, customizing API permissions requires unpacking the distribution and modifying this file consistently on all servers. The old system also does not permit the specification of additional roles. FS: https://cwiki.apache.org/confluence/display/CLOUDSTACK/Dynamic+Role+Based+API+Access+Checker+for+CloudStack DB-Backed Dynamic Role Based API Access Checker for CloudStack brings following changes, features and use-cases: - Moves the API access definitions from commands.properties to the mgmt server DB - Allows defining custom roles (such as a read-only ROOT admin) beyond the current set of four (4) roles - All roles will resolve to one of the four known roles types (Admin, Resource Admin, Domain Admin and User) which maintains this association by requiring all new defined roles to specify a role type. - Allows changes to roles and API permissions per role at runtime including additions or removal of roles and/or modifications of permissions, without the need of restarting management server(s) Upgrade/installation notes: - The feature will be enabled by default for new installations, existing deployments will continue to use the older static role based api access checker with an option to enable this feature - During fresh installation or upgrade, the upgrade paths will add four default roles based on the four default role types - For ease of migration, at the time of upgrade commands.properties will be used to add existing set of permissions to the default roles. cloud.account will have a new role_id column which will be populated based on default roles as well Dynamic-roles migration tool: scripts/util/migrate-dynamicroles.py - Allows admins to migrate to the dynamic role based checker at a future date - Performs a harder one-way migrate and update - Migrates rules from existing commands.properties file into db and deprecates it - Enables an internal hidden switch to enable dynamic role based checker feature Signed-off-by: Rohit Yadav <[email protected]>
1 parent 0dcaf19 commit 95e0b8d

File tree

90 files changed

+5254
-152
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

90 files changed

+5254
-152
lines changed

.travis.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,10 @@ env:
3636
- REGRESSION_INDEX=6
3737
- PATH=$HOME/.local/bin:$PATH
3838
matrix:
39-
- TESTS="smoke/test_affinity_groups smoke/test_affinity_groups_projects smoke/test_deploy_vgpu_enabled_vm smoke/test_deploy_vm_iso smoke/test_deploy_vm_root_resize smoke/test_deploy_vm_with_userdata smoke/test_deploy_vms_with_varied_deploymentplanners smoke/test_disk_offerings smoke/test_global_settings smoke/test_guest_vlan_range"
39+
- TESTS="smoke/test_affinity_groups smoke/test_affinity_groups_projects smoke/test_dynamicroles smoke/test_deploy_vgpu_enabled_vm smoke/test_deploy_vm_iso smoke/test_deploy_vm_root_resize smoke/test_deploy_vm_with_userdata smoke/test_deploy_vms_with_varied_deploymentplanners smoke/test_disk_offerings smoke/test_global_settings smoke/test_guest_vlan_range"
4040
- TESTS="smoke/test_hosts smoke/test_internal_lb smoke/test_iso smoke/test_loadbalance smoke/test_multipleips_per_nic smoke/test_network smoke/test_network_acl smoke/test_nic smoke/test_nic_adapter_type smoke/test_non_contigiousvlan"
4141
- TESTS="smoke/test_over_provisioning smoke/test_password_server smoke/test_portable_publicip smoke/test_primary_storage smoke/test_privategw_acl smoke/test_public_ip_range smoke/test_pvlan smoke/test_regions smoke/test_reset_vm_on_reboot smoke/test_resource_detail"
42-
- TESTS="smoke/test_router_dhcphosts smoke/test_routers smoke/test_routers_iptables_default_policy smoke/test_routers_network_ops smoke/test_scale_vm smoke/test_secondary_storage smoke/test_service_offerings smoke/test_snapshots smoke/test_ssvm smoke/test_templates"
42+
- TESTS="smoke/test_router_dhcphosts smoke/test_routers smoke/test_routers_iptables_default_policy smoke/test_routers_network_ops smoke/test_staticroles smoke/test_scale_vm smoke/test_secondary_storage smoke/test_service_offerings smoke/test_snapshots smoke/test_ssvm smoke/test_templates"
4343
- TESTS="smoke/test_usage_events smoke/test_vm_life_cycle smoke/test_vm_snapshots smoke/test_volumes smoke/test_vpc_redundant smoke/test_vpc_router_nics smoke/test_vpc_vpn smoke/misc/test_deploy_vm smoke/misc/test_vm_ha smoke/misc/test_escalations_templates smoke/misc/test_vm_sync"
4444

4545
- TESTS="component/test_mm_max_limits component/test_acl_isolatednetwork_delete"

api/src/com/cloud/event/EventTypes.java

+18
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@
6666
import com.cloud.vm.Nic;
6767
import com.cloud.vm.NicSecondaryIp;
6868
import com.cloud.vm.VirtualMachine;
69+
import org.apache.cloudstack.acl.Role;
70+
import org.apache.cloudstack.acl.RolePermission;
6971
import org.apache.cloudstack.config.Configuration;
7072
import org.apache.cloudstack.usage.Usage;
7173

@@ -166,6 +168,14 @@ public class EventTypes {
166168
public static final String EVENT_GLOBAL_LOAD_BALANCER_DELETE = "GLOBAL.LB.DELETE";
167169
public static final String EVENT_GLOBAL_LOAD_BALANCER_UPDATE = "GLOBAL.LB.UPDATE";
168170

171+
// Role events
172+
public static final String EVENT_ROLE_CREATE = "ROLE.CREATE";
173+
public static final String EVENT_ROLE_UPDATE = "ROLE.UPDATE";
174+
public static final String EVENT_ROLE_DELETE = "ROLE.DELETE";
175+
public static final String EVENT_ROLE_PERMISSION_CREATE = "ROLE.PERMISSION.CREATE";
176+
public static final String EVENT_ROLE_PERMISSION_UPDATE = "ROLE.PERMISSION.UPDATE";
177+
public static final String EVENT_ROLE_PERMISSION_DELETE = "ROLE.PERMISSION.DELETE";
178+
169179
// Account events
170180
public static final String EVENT_ACCOUNT_ENABLE = "ACCOUNT.ENABLE";
171181
public static final String EVENT_ACCOUNT_DISABLE = "ACCOUNT.DISABLE";
@@ -605,6 +615,14 @@ public class EventTypes {
605615
entityEventDetails.put(EVENT_LB_CERT_ASSIGN, LoadBalancer.class);
606616
entityEventDetails.put(EVENT_LB_CERT_REMOVE, LoadBalancer.class);
607617

618+
// Role events
619+
entityEventDetails.put(EVENT_ROLE_CREATE, Role.class);
620+
entityEventDetails.put(EVENT_ROLE_UPDATE, Role.class);
621+
entityEventDetails.put(EVENT_ROLE_DELETE, Role.class);
622+
entityEventDetails.put(EVENT_ROLE_PERMISSION_CREATE, RolePermission.class);
623+
entityEventDetails.put(EVENT_ROLE_PERMISSION_UPDATE, RolePermission.class);
624+
entityEventDetails.put(EVENT_ROLE_PERMISSION_DELETE, RolePermission.class);
625+
608626
// Account events
609627
entityEventDetails.put(EVENT_ACCOUNT_ENABLE, Account.class);
610628
entityEventDetails.put(EVENT_ACCOUNT_DISABLE, Account.class);

api/src/com/cloud/user/Account.java

+2
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ public enum State {
4646

4747
public short getType();
4848

49+
public Long getRoleId();
50+
4951
public State getState();
5052

5153
public Date getRemoved();

api/src/com/cloud/user/AccountService.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,9 @@ public interface AccountService {
5757
* @return the user if created successfully, null otherwise
5858
*/
5959
UserAccount createUserAccount(String userName, String password, String firstName, String lastName, String email, String timezone, String accountName,
60-
short accountType, Long domainId, String networkDomain, Map<String, String> details, String accountUUID, String userUUID);
60+
short accountType, Long roleId, Long domainId, String networkDomain, Map<String, String> details, String accountUUID, String userUUID);
6161

62-
UserAccount createUserAccount(String userName, String password, String firstName, String lastName, String email, String timezone, String accountName, short accountType, Long domainId, String networkDomain,
62+
UserAccount createUserAccount(String userName, String password, String firstName, String lastName, String email, String timezone, String accountName, short accountType, Long roleId, Long domainId, String networkDomain,
6363
Map<String, String> details, String accountUUID, String userUUID, User.Source source);
6464

6565
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
package org.apache.cloudstack.acl;
19+
20+
import org.apache.cloudstack.api.Identity;
21+
import org.apache.cloudstack.api.InternalIdentity;
22+
23+
public interface Role extends InternalIdentity, Identity {
24+
String getName();
25+
RoleType getRoleType();
26+
String getDescription();
27+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
package org.apache.cloudstack.acl;
19+
20+
import org.apache.cloudstack.api.Identity;
21+
import org.apache.cloudstack.api.InternalIdentity;
22+
23+
public interface RolePermission extends InternalIdentity, Identity {
24+
enum Permission {ALLOW, DENY}
25+
26+
long getRoleId();
27+
String getRule();
28+
Permission getPermission();
29+
String getDescription();
30+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
package org.apache.cloudstack.acl;
19+
20+
import org.apache.cloudstack.framework.config.ConfigKey;
21+
22+
import java.util.List;
23+
24+
public interface RoleService {
25+
26+
ConfigKey<Boolean> EnableDynamicApiChecker = new ConfigKey<>("Hidden", Boolean.class, "dynamic.apichecker.enabled", "false",
27+
"If set to true, this enables the dynamic role-based api access checker and disables the default static role-based api access checker.",
28+
true);
29+
30+
boolean isEnabled();
31+
Role findRole(final Long id);
32+
Role createRole(final String name, final RoleType roleType, final String description);
33+
boolean updateRole(final Role role, final String name, final RoleType roleType, final String description);
34+
boolean deleteRole(final Role role);
35+
36+
RolePermission findRolePermission(final Long id);
37+
RolePermission createRolePermission(final Role role, final Rule rule, final RolePermission.Permission permission, final String description);
38+
boolean updateRolePermission(final RolePermission rolePermission, final Rule rule, final RolePermission.Permission permission, final String description);
39+
boolean deleteRolePermission(final RolePermission rolePermission);
40+
41+
List<Role> findAllRolesBy(final Long id, final String name, final RoleType roleType);
42+
List<RolePermission> findAllPermissionsBy(final Long roleId);
43+
}

api/src/org/apache/cloudstack/acl/RoleType.java

+76-4
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,90 @@
1616
// under the License.
1717
package org.apache.cloudstack.acl;
1818

19+
import com.cloud.user.Account;
20+
import com.google.common.base.Enums;
21+
import com.google.common.base.Strings;
22+
1923
// Enum for default roles in CloudStack
2024
public enum RoleType {
21-
Admin(1), ResourceAdmin(2), DomainAdmin(4), User(8), Unknown(0);
25+
Admin(1L, Account.ACCOUNT_TYPE_ADMIN, 1),
26+
ResourceAdmin(2L, Account.ACCOUNT_TYPE_RESOURCE_DOMAIN_ADMIN, 2),
27+
DomainAdmin(3L, Account.ACCOUNT_TYPE_DOMAIN_ADMIN, 4),
28+
User(4L, Account.ACCOUNT_TYPE_NORMAL, 8),
29+
Unknown(-1L, (short) -1, 0);
2230

31+
private long id;
32+
private short accountType;
2333
private int mask;
2434

25-
private RoleType(int mask) {
35+
RoleType(final long id, final short accountType, final int mask) {
36+
this.id = id;
37+
this.accountType = accountType;
2638
this.mask = mask;
2739
}
2840

29-
public int getValue() {
41+
public long getId() {
42+
return id;
43+
}
44+
45+
public short getAccountType() {
46+
return accountType;
47+
}
48+
49+
public int getMask() {
3050
return mask;
3151
}
32-
}
3352

53+
public static RoleType fromString(final String name) {
54+
if (!Strings.isNullOrEmpty(name)
55+
&& Enums.getIfPresent(RoleType.class, name).isPresent()) {
56+
return RoleType.valueOf(name);
57+
}
58+
return null;
59+
}
60+
61+
public static RoleType fromMask(int mask) {
62+
for (RoleType roleType : RoleType.values()) {
63+
if (roleType.getMask() == mask) {
64+
return roleType;
65+
}
66+
}
67+
return Unknown;
68+
}
69+
70+
public static RoleType getByAccountType(final short accountType) {
71+
RoleType roleType = RoleType.Unknown;
72+
switch (accountType) {
73+
case Account.ACCOUNT_TYPE_ADMIN:
74+
roleType = RoleType.Admin;
75+
break;
76+
case Account.ACCOUNT_TYPE_DOMAIN_ADMIN:
77+
roleType = RoleType.DomainAdmin;
78+
break;
79+
case Account.ACCOUNT_TYPE_RESOURCE_DOMAIN_ADMIN:
80+
roleType = RoleType.ResourceAdmin;
81+
break;
82+
case Account.ACCOUNT_TYPE_NORMAL:
83+
roleType = RoleType.User;
84+
break;
85+
}
86+
return roleType;
87+
}
88+
89+
public static Long getRoleByAccountType(final Long roleId, final Short accountType) {
90+
if (roleId == null && accountType != null) {
91+
RoleType defaultRoleType = RoleType.getByAccountType(accountType);
92+
if (defaultRoleType != null && defaultRoleType != RoleType.Unknown) {
93+
return defaultRoleType.getId();
94+
}
95+
}
96+
return roleId;
97+
}
98+
99+
public static Short getAccountTypeByRole(final Role role, final Short accountType) {
100+
if (role != null && role.getId() > 0L) {
101+
return role.getRoleType().getAccountType();
102+
}
103+
return accountType;
104+
}
105+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
package org.apache.cloudstack.acl;
19+
20+
import com.cloud.exception.InvalidParameterValueException;
21+
import com.google.common.base.Strings;
22+
23+
public final class Rule {
24+
private final String rule;
25+
private final static String ALLOWED_PATTERN = "^[a-zA-Z0-9*]+$";
26+
27+
public Rule(final String rule) {
28+
validate(rule);
29+
this.rule = rule;
30+
}
31+
32+
public String toString() {
33+
return rule;
34+
}
35+
36+
public static boolean validate(final String rule) throws InvalidParameterValueException {
37+
if (Strings.isNullOrEmpty(rule) || !rule.matches(ALLOWED_PATTERN)) {
38+
throw new InvalidParameterValueException("Invalid rule provided. Only API names and wildcards are allowed.");
39+
}
40+
return true;
41+
}
42+
}

api/src/org/apache/cloudstack/api/ApiConstants.java

+5
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,11 @@ public class ApiConstants {
358358
public static final String PROJECT_IDS = "projectids";
359359
public static final String PROJECT = "project";
360360
public static final String ROLE = "role";
361+
public static final String ROLE_ID = "roleid";
362+
public static final String ROLE_TYPE = "roletype";
363+
public static final String ROLE_NAME = "rolename";
364+
public static final String PERMISSION = "permission";
365+
public static final String RULE = "rule";
361366
public static final String USER = "user";
362367
public static final String ACTIVE_ONLY = "activeonly";
363368
public static final String TOKEN = "token";

api/src/org/apache/cloudstack/api/BaseCmd.java

+5-2
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import javax.inject.Inject;
3131

3232
import com.cloud.utils.HttpUtils;
33+
import org.apache.cloudstack.acl.RoleService;
3334
import org.apache.log4j.Logger;
3435

3536
import org.apache.cloudstack.acl.RoleType;
@@ -85,6 +86,7 @@
8586

8687
public abstract class BaseCmd {
8788
private static final Logger s_logger = Logger.getLogger(BaseCmd.class.getName());
89+
public static final String RESPONSE_SUFFIX = "response";
8890
public static final String RESPONSE_TYPE_XML = HttpUtils.RESPONSE_TYPE_XML;
8991
public static final String RESPONSE_TYPE_JSON = HttpUtils.RESPONSE_TYPE_JSON;
9092
public static final String USER_ERROR_MESSAGE = "Internal error executing command, please contact your system administrator";
@@ -104,12 +106,13 @@ public static enum CommandType {
104106
@Parameter(name = "response", type = CommandType.STRING)
105107
private String responseType;
106108

107-
108109
@Inject
109110
public ConfigurationService _configService;
110111
@Inject
111112
public AccountService _accountService;
112113
@Inject
114+
public RoleService roleService;
115+
@Inject
113116
public UserVmService _userVmService;
114117
@Inject
115118
public ManagementService _mgr;
@@ -323,7 +326,7 @@ public List<Field> getParamFields() {
323326
if (allowedRoles.length > 0) {
324327
roleIsAllowed = false;
325328
for (final RoleType allowedRole : allowedRoles) {
326-
if (allowedRole.getValue() == caller.getType()) {
329+
if (allowedRole.getAccountType() == caller.getType()) {
327330
roleIsAllowed = true;
328331
break;
329332
}

0 commit comments

Comments
 (0)