blob: 9b6ac0e0279777aff4f37040c2dbd40aba856157 [file] [log] [blame]
// 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 java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import javax.inject.Inject;
import org.apache.cloudstack.acl.dao.RoleDao;
import org.apache.cloudstack.acl.dao.RolePermissionsDao;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.ApiErrorCode;
import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.command.admin.acl.CreateRoleCmd;
import org.apache.cloudstack.api.command.admin.acl.CreateRolePermissionCmd;
import org.apache.cloudstack.api.command.admin.acl.DeleteRoleCmd;
import org.apache.cloudstack.api.command.admin.acl.DeleteRolePermissionCmd;
import org.apache.cloudstack.api.command.admin.acl.ImportRoleCmd;
import org.apache.cloudstack.api.command.admin.acl.ListRolePermissionsCmd;
import org.apache.cloudstack.api.command.admin.acl.ListRolesCmd;
import org.apache.cloudstack.api.command.admin.acl.UpdateRoleCmd;
import org.apache.cloudstack.api.command.admin.acl.UpdateRolePermissionCmd;
import org.apache.cloudstack.acl.RolePermissionEntity.Permission;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.framework.config.ConfigKey;
import org.apache.cloudstack.framework.config.Configurable;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import com.cloud.event.ActionEvent;
import com.cloud.event.EventTypes;
import com.cloud.exception.PermissionDeniedException;
import com.cloud.user.Account;
import com.cloud.user.AccountManager;
import com.cloud.user.dao.AccountDao;
import com.cloud.utils.ListUtils;
import com.cloud.utils.Pair;
import com.cloud.utils.component.ManagerBase;
import com.cloud.utils.component.PluggableService;
import com.cloud.utils.db.Transaction;
import com.cloud.utils.db.TransactionCallback;
import com.cloud.utils.db.TransactionStatus;
import com.cloud.utils.exception.CloudRuntimeException;
public class RoleManagerImpl extends ManagerBase implements RoleService, Configurable, PluggableService {
@Inject
private AccountDao accountDao;
@Inject
private RoleDao roleDao;
@Inject
private RolePermissionsDao rolePermissionsDao;
@Inject
private AccountManager accountManager;
public void checkCallerAccess() {
if (!isEnabled()) {
throw new PermissionDeniedException("Dynamic api checker is not enabled, aborting role operation");
}
Account caller = getCurrentAccount();
if (caller == null || caller.getRoleId() == null) {
throw new PermissionDeniedException("Restricted API called by an invalid user account");
}
Role callerRole = findRole(caller.getRoleId());
if (callerRole == null || callerRole.getRoleType() != RoleType.Admin) {
throw new PermissionDeniedException("Restricted API called by a user account of non-Admin role type");
}
}
@Override
public boolean isEnabled() {
return RoleService.EnableDynamicApiChecker.value();
}
@Override
public Role findRole(Long id, boolean ignorePrivateRoles) {
if (id == null || id < 1L) {
logger.trace(String.format("Role ID is invalid [%s]", id));
return null;
}
RoleVO role = roleDao.findById(id);
if (role == null) {
logger.trace(String.format("Role not found [id=%s]", id));
return null;
}
if (!isCallerRootAdmin() && (RoleType.Admin == role.getRoleType() || (!role.isPublicRole() && ignorePrivateRoles))) {
logger.debug(String.format("Role [id=%s, name=%s] is either of 'Admin' type or is private and is only visible to 'Root admins'.", id, role.getName()));
return null;
}
return role;
}
@Override
public List<Role> findRoles(List<Long> ids, boolean ignorePrivateRoles) {
List<Role> result = new ArrayList<>();
if (CollectionUtils.isEmpty(ids)) {
logger.trace(String.format("Role IDs are invalid [%s]", ids));
return result;
}
List<RoleVO> roles = roleDao.searchByIds(ids.toArray(new Long[0]));
if (CollectionUtils.isEmpty(roles)) {
logger.trace(String.format("Roles not found [ids=%s]", ids));
return result;
}
for (Role role : roles) {
if (!isCallerRootAdmin() && (RoleType.Admin == role.getRoleType() || (!role.isPublicRole() && ignorePrivateRoles))) {
logger.debug(String.format("Role [id=%s, name=%s] is either of 'Admin' type or is private and is only visible to 'Root admins'.", role.getId(), role.getName()));
continue;
}
result.add(role);
}
return result;
}
@Override
public Role findRole(Long id) {
return findRole(id, false);
}
/**
* Simple call to {@link CallContext#current()} to retrieve the current calling account.
* This method facilitates unit testing, it avoids mocking static methods.
*/
protected Account getCurrentAccount() {
return CallContext.current().getCallingAccount();
}
@Override
public RolePermission findRolePermission(final Long id) {
if (id == null) {
return null;
}
return rolePermissionsDao.findById(id);
}
@Override
public RolePermission findRolePermissionByRoleIdAndRule(final Long roleId, final String rule) {
if (roleId == null || StringUtils.isEmpty(rule)) {
return null;
}
return rolePermissionsDao.findByRoleIdAndRule(roleId, rule);
}
@Override
@ActionEvent(eventType = EventTypes.EVENT_ROLE_CREATE, eventDescription = "creating Role")
public Role createRole(final String name, final RoleType roleType, final String description, boolean publicRole) {
checkCallerAccess();
if (roleType == null || roleType == RoleType.Unknown) {
throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Invalid role type provided");
}
return Transaction.execute(new TransactionCallback<RoleVO>() {
@Override
public RoleVO doInTransaction(TransactionStatus status) {
RoleVO role = new RoleVO(name, roleType, description);
role.setPublicRole(publicRole);
role = roleDao.persist(role);
CallContext.current().putContextParameter(Role.class, role.getUuid());
return role;
}
});
}
@Override
@ActionEvent(eventType = EventTypes.EVENT_ROLE_CREATE, eventDescription = "creating role by cloning another role")
public Role createRole(String name, Role role, String description, boolean publicRole) {
checkCallerAccess();
return Transaction.execute(new TransactionCallback<RoleVO>() {
@Override
public RoleVO doInTransaction(TransactionStatus status) {
RoleVO newRole = new RoleVO(name, role.getRoleType(), description);
newRole.setPublicRole(publicRole);
RoleVO newRoleVO = roleDao.persist(newRole);
if (newRoleVO == null) {
throw new CloudRuntimeException("Unable to create the role: " + name + ", failed to persist in DB");
}
List<RolePermissionVO> rolePermissions = rolePermissionsDao.findAllByRoleIdSorted(role.getId());
if (rolePermissions != null && !rolePermissions.isEmpty()) {
for (RolePermissionVO permission : rolePermissions) {
rolePermissionsDao.persist(new RolePermissionVO(newRoleVO.getId(), permission.getRule().toString(), permission.getPermission(), permission.getDescription()));
}
}
return newRoleVO;
}
});
}
@Override
@ActionEvent(eventType = EventTypes.EVENT_ROLE_IMPORT, eventDescription = "importing Role")
public Role importRole(String name, RoleType type, String description, List<Map<String, Object>> rules, boolean forced, boolean isPublicRole) {
checkCallerAccess();
if (StringUtils.isEmpty(name)) {
throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Invalid role name provided");
}
if (type == null || type == RoleType.Unknown) {
throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Invalid role type provided");
}
List<RoleVO> existingRoles = roleDao.findByName(name, isCallerRootAdmin());
if (CollectionUtils.isNotEmpty(existingRoles) && !forced) {
throw new CloudRuntimeException("Role already exists");
}
return Transaction.execute(new TransactionCallback<RoleVO>() {
@Override
public RoleVO doInTransaction(TransactionStatus status) {
RoleVO newRole = null;
RoleVO existingRole = roleDao.findByNameAndType(name, type, isCallerRootAdmin());
if (existingRole != null) {
if (existingRole.isDefault()) {
throw new CloudRuntimeException("Failed to import the role: " + name + ", default role cannot be overriden");
}
//Cleanup old role permissions
List<? extends RolePermission> rolePermissions = rolePermissionsDao.findAllByRoleIdSorted(existingRole.getId());
if (rolePermissions != null && !rolePermissions.isEmpty()) {
for (RolePermission rolePermission : rolePermissions) {
rolePermissionsDao.remove(rolePermission.getId());
}
}
existingRole.setName(name);
existingRole.setRoleType(type);
existingRole.setDescription(description);
existingRole.setPublicRole(isPublicRole);
roleDao.update(existingRole.getId(), existingRole);
newRole = existingRole;
} else {
RoleVO role = new RoleVO(name, type, description);
role.setPublicRole(isPublicRole);
newRole = roleDao.persist(role);
}
if (newRole == null) {
throw new CloudRuntimeException("Unable to import the role: " + name + ", failed to persist in DB");
}
if (rules != null && !rules.isEmpty()) {
for (Map<String, Object> ruleDetail : rules) {
Rule rule = (Rule)ruleDetail.get(ApiConstants.RULE);
RolePermission.Permission rulePermission = (RolePermission.Permission) ruleDetail.get(ApiConstants.PERMISSION);
String ruleDescription = (String) ruleDetail.get(ApiConstants.DESCRIPTION);
rolePermissionsDao.persist(new RolePermissionVO(newRole.getId(), rule.toString(), rulePermission, ruleDescription));
}
}
return newRole;
}
});
}
@Override
@ActionEvent(eventType = EventTypes.EVENT_ROLE_UPDATE, eventDescription = "updating Role")
public Role updateRole(final Role role, final String name, final RoleType roleType, final String description, Boolean publicRole) {
checkCallerAccess();
if (roleType != null && roleType == RoleType.Unknown) {
throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Unknown is not a valid role type");
}
RoleVO roleVO = (RoleVO)role;
if (role.isDefault()) {
if (publicRole == null || roleType != null || !StringUtils.isAllEmpty(name, description)) {
throw new PermissionDeniedException("Default roles cannot be updated (with the exception of making it private/public).");
}
roleVO.setPublicRole(publicRole);
roleDao.update(role.getId(), roleVO);
return role;
}
if (StringUtils.isNotEmpty(name)) {
roleVO.setName(name);
}
if (roleType != null) {
List<? extends Account> accounts = accountDao.findAccountsByRole(role.getId());
if (accounts == null || accounts.isEmpty()) {
roleVO.setRoleType(roleType);
} else {
throw new PermissionDeniedException("Found accounts that have role in use, won't allow to change role type");
}
}
if (StringUtils.isNotEmpty(description)) {
roleVO.setDescription(description);
}
if (publicRole == null) {
publicRole = role.isPublicRole();
}
roleVO.setPublicRole(publicRole);
roleDao.update(role.getId(), roleVO);
return role;
}
@Override
@ActionEvent(eventType = EventTypes.EVENT_ROLE_DELETE, eventDescription = "deleting Role")
public boolean deleteRole(final Role role) {
checkCallerAccess();
if (role == null) {
return false;
}
if (role.isDefault()) {
throw new PermissionDeniedException("Default roles cannot be deleted");
}
List<? extends Account> accounts = accountDao.findAccountsByRole(role.getId());
if (accounts == null || accounts.size() == 0) {
return Transaction.execute(new TransactionCallback<Boolean>() {
@Override
public Boolean doInTransaction(TransactionStatus status) {
List<? extends RolePermission> rolePermissions = rolePermissionsDao.findAllByRoleIdSorted(role.getId());
if (rolePermissions != null && !rolePermissions.isEmpty()) {
for (RolePermission rolePermission : rolePermissions) {
rolePermissionsDao.remove(rolePermission.getId());
}
}
if (roleDao.remove(role.getId())) {
RoleVO roleVO = roleDao.findByIdIncludingRemoved(role.getId());
roleVO.setName(null);
return roleDao.update(role.getId(), roleVO);
}
return false;
}
});
}
throw new PermissionDeniedException("Found accounts that have role in use, won't allow to delete role");
}
@Override
@ActionEvent(eventType = EventTypes.EVENT_ROLE_PERMISSION_CREATE, eventDescription = "creating Role Permission")
public RolePermission createRolePermission(final Role role, final Rule rule, final Permission permission, final String description) {
checkCallerAccess();
if (role.isDefault()) {
throw new PermissionDeniedException("Role permission cannot be added for Default roles");
}
if (findRolePermissionByRoleIdAndRule(role.getId(), rule.toString()) != null) {
throw new PermissionDeniedException("Rule already exists for the role: " + role.getName());
}
return Transaction.execute(new TransactionCallback<RolePermissionVO>() {
@Override
public RolePermissionVO doInTransaction(TransactionStatus status) {
return rolePermissionsDao.persist(new RolePermissionVO(role.getId(), rule.toString(), permission, description));
}
});
}
@Override
@ActionEvent(eventType = EventTypes.EVENT_ROLE_PERMISSION_UPDATE, eventDescription = "updating Role Permission order")
public boolean updateRolePermission(final Role role, final List<RolePermission> newOrder) {
checkCallerAccess();
if (role.isDefault()) {
throw new PermissionDeniedException("Role permission cannot be updated for Default roles");
}
return role != null && newOrder != null && rolePermissionsDao.update(role, newOrder);
}
@Override
public boolean updateRolePermission(Role role, RolePermission rolePermission, Permission permission) {
checkCallerAccess();
if (role.isDefault()) {
throw new PermissionDeniedException("Role permission cannot be updated for Default roles");
}
return role != null && rolePermissionsDao.update(role, rolePermission, permission);
}
@Override
@ActionEvent(eventType = EventTypes.EVENT_ROLE_PERMISSION_DELETE, eventDescription = "deleting Role Permission")
public boolean deleteRolePermission(final RolePermission rolePermission) {
checkCallerAccess();
Role role = findRole(rolePermission.getRoleId());
if (role.isDefault()) {
throw new PermissionDeniedException("Role permission cannot be deleted for Default roles");
}
return rolePermission != null && rolePermissionsDao.remove(rolePermission.getId());
}
@Override
public List<Role> findRolesByName(String name) {
return findRolesByName(name, null, null, null).first();
}
@Override
public Pair<List<Role>, Integer> findRolesByName(String name, String keyword, Long startIndex, Long limit) {
if (StringUtils.isNotBlank(name) || StringUtils.isNotBlank(keyword)) {
Pair<List<RoleVO>, Integer> data = roleDao.findAllByName(name, keyword, startIndex, limit, isCallerRootAdmin());
int removed = removeRootAdminRolesIfNeeded(data.first());
return new Pair<List<Role>,Integer>(ListUtils.toListOfInterface(data.first()), Integer.valueOf(data.second() - removed));
}
return new Pair<List<Role>, Integer>(new ArrayList<Role>(), 0);
}
/**
* Removes roles of the given list that have the type '{@link RoleType#Admin}' if the user calling the method is not a 'root admin'.
* The actual removal is executed via {@link #removeRootAdminRoles(List)}. Therefore, if the method is called by a 'root admin', we do nothing here.
*/
protected int removeRootAdminRolesIfNeeded(List<? extends Role> roles) {
if (!isCallerRootAdmin()) {
return removeRootAdminRoles(roles);
}
return 0;
}
/**
* Remove all roles that have the {@link RoleType#Admin}.
*/
protected int removeRootAdminRoles(List<? extends Role> roles) {
if (CollectionUtils.isEmpty(roles)) {
return 0;
}
Iterator<? extends Role> rolesIterator = roles.iterator();
int count = 0;
while (rolesIterator.hasNext()) {
Role role = rolesIterator.next();
if (RoleType.Admin == role.getRoleType()) {
count++;
rolesIterator.remove();
}
}
return count;
}
@Override
public List<Role> findRolesByType(RoleType roleType) {
return findRolesByType(roleType, null, null).first();
}
@Override
public Pair<List<Role>, Integer> findRolesByType(RoleType roleType, Long startIndex, Long limit) {
if (roleType == null || RoleType.Admin == roleType && !isCallerRootAdmin()) {
return new Pair<List<Role>, Integer>(Collections.emptyList(), 0);
}
Pair<List<RoleVO>, Integer> data = roleDao.findAllByRoleType(roleType, startIndex, limit, isCallerRootAdmin());
return new Pair<List<Role>,Integer>(ListUtils.toListOfInterface(data.first()), Integer.valueOf(data.second()));
}
@Override
public List<Role> listRoles() {
List<? extends Role> roles = roleDao.listAll();
removeRootAdminRolesIfNeeded(roles);
return ListUtils.toListOfInterface(roles);
}
@Override
public Pair<List<Role>, Integer> listRoles(Long startIndex, Long limit) {
Pair<List<RoleVO>, Integer> data = roleDao.listAllRoles(startIndex, limit, isCallerRootAdmin());
int removed = removeRootAdminRolesIfNeeded(data.first());
return new Pair<List<Role>,Integer>(ListUtils.toListOfInterface(data.first()), Integer.valueOf(data.second() - removed));
}
@Override
public List<RolePermission> findAllPermissionsBy(final Long roleId) {
List<? extends RolePermission> permissions = rolePermissionsDao.findAllByRoleIdSorted(roleId);
if (permissions != null) {
return new ArrayList<>(permissions);
}
return Collections.emptyList();
}
private boolean isCallerRootAdmin() {
return accountManager.isRootAdmin(getCurrentAccount().getId());
}
@Override
public Permission getRolePermission(String permission) {
if (StringUtils.isEmpty(permission)) {
return null;
}
if (!permission.equalsIgnoreCase(RolePermission.Permission.ALLOW.toString()) &&
!permission.equalsIgnoreCase(RolePermission.Permission.DENY.toString())) {
throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Values for permission parameter should be: allow or deny");
}
return permission.equalsIgnoreCase(RolePermission.Permission.ALLOW.toString()) ? RolePermission.Permission.ALLOW : RolePermission.Permission.DENY;
}
@Override
public String getConfigComponentName() {
return RoleService.class.getSimpleName();
}
@Override
public ConfigKey<?>[] getConfigKeys() {
return new ConfigKey<?>[] {RoleService.EnableDynamicApiChecker};
}
@Override
public List<Class<?>> getCommands() {
final List<Class<?>> cmdList = new ArrayList<>();
cmdList.add(CreateRoleCmd.class);
cmdList.add(ImportRoleCmd.class);
cmdList.add(ListRolesCmd.class);
cmdList.add(UpdateRoleCmd.class);
cmdList.add(DeleteRoleCmd.class);
cmdList.add(CreateRolePermissionCmd.class);
cmdList.add(ListRolePermissionsCmd.class);
cmdList.add(UpdateRolePermissionCmd.class);
cmdList.add(DeleteRolePermissionCmd.class);
return cmdList;
}
}