Skip to content

Commit bee2bdc

Browse files
committed
Merge pull request #1489 from shapeblue/dynamicroles-master
CLOUDSTACK-8562: Dynamic Role-Based API Checker for CloudStack### 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 * pr/1489: maven: Fix jstl version usage CLOUDSTACK-8562: Deprecate commands.properties CLOUDSTACK-8562: DB-Backed Dynamic Role Based API Access Checker CLOUDSTACK-9361: Centrally handle API validations Signed-off-by: Will Stevens <williamstevens@gmail.com>
2 parents dc5b529 + 003b97b commit bee2bdc

116 files changed

Lines changed: 5793 additions & 1542 deletions

File tree

Some content is hidden

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

.travis.yml

Lines changed: 2 additions & 2 deletions
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_list_ids_parameter 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

Lines changed: 18 additions & 0 deletions
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

Lines changed: 2 additions & 0 deletions
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

Lines changed: 2 additions & 2 deletions
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
/**
Lines changed: 27 additions & 0 deletions
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+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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+
Rule getRule();
28+
Permission getPermission();
29+
String getDescription();
30+
long getSortOrder();
31+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
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<>("Advanced", 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 findRolePermissionByUuid(final String uuid);
38+
39+
RolePermission createRolePermission(final Role role, final Rule rule, final RolePermission.Permission permission, final String description);
40+
/**
41+
* updateRolePermission updates the order/position of an role permission
42+
* @param role The role whose permissions needs to be re-ordered
43+
* @param newOrder The new list of ordered role permissions
44+
*/
45+
boolean updateRolePermission(final Role role, final List<RolePermission> newOrder);
46+
boolean deleteRolePermission(final RolePermission rolePermission);
47+
48+
List<Role> listRoles();
49+
List<Role> findRolesByName(final String name);
50+
List<Role> findRolesByType(final RoleType roleType);
51+
List<RolePermission> findAllPermissionsBy(final Long roleId);
52+
}

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

Lines changed: 76 additions & 4 deletions
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+
throw new IllegalStateException("Illegal RoleType name provided");
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+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
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+
import java.util.regex.Pattern;
24+
25+
public final class Rule {
26+
private final String rule;
27+
private final static Pattern ALLOWED_PATTERN = Pattern.compile("^[a-zA-Z0-9*]+$");
28+
29+
public Rule(final String rule) {
30+
validate(rule);
31+
this.rule = rule;
32+
}
33+
34+
public boolean matches(final String commandName) {
35+
return !Strings.isNullOrEmpty(commandName)
36+
&& commandName.toLowerCase().matches(rule.toLowerCase().replace("*", "\\w*"));
37+
}
38+
39+
public String getRuleString() {
40+
return rule;
41+
}
42+
43+
@Override
44+
public String toString() {
45+
return rule;
46+
}
47+
48+
private static boolean validate(final String rule) {
49+
if (Strings.isNullOrEmpty(rule) || !ALLOWED_PATTERN.matcher(rule).matches()) {
50+
throw new InvalidParameterValueException("Only API names and wildcards are allowed, invalid rule provided: " + rule);
51+
}
52+
return true;
53+
}
54+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
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.api;
19+
20+
public enum ApiArgValidator {
21+
NotNullOrEmpty, // does Strings.isNullOrEmpty check
22+
PositiveNumber, // does != null and > 0 check
23+
}

0 commit comments

Comments
 (0)