/* license-start
*
* Copyright (C) 2008 - 2013 Crispico, <http://www.crispico.com/>.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details, at <http://www.gnu.org/licenses/>.
*
* Contributors:
* Crispico - Initial API and implementation
*
* license-end
*/
package org.flowerplatform.web.security.service;
import java.io.File;
import java.lang.reflect.Constructor;
import java.security.Permission;
import java.security.Policy;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Observable;
import java.util.Observer;
import java.util.Set;
import java.util.regex.Matcher;
import org.flowerplatform.common.CommonPlugin;
import org.flowerplatform.common.FlowerProperties.AddBooleanProperty;
import org.flowerplatform.communication.CommunicationPlugin;
import org.flowerplatform.communication.service.ServiceInvocationContext;
import org.flowerplatform.communication.service.ServiceRegistry;
import org.flowerplatform.web.database.DatabaseOperation;
import org.flowerplatform.web.database.DatabaseOperationWrapper;
import org.flowerplatform.web.entity.Entity;
import org.flowerplatform.web.entity.EntityFactory;
import org.flowerplatform.web.entity.Group;
import org.flowerplatform.web.entity.ISecurityEntity;
import org.flowerplatform.web.entity.PermissionEntity;
import org.flowerplatform.web.security.dto.PermissionAdminUIDto;
import org.flowerplatform.web.security.dto.PermissionsByResourceFilter;
import org.flowerplatform.web.security.permission.AdminSecurityEntitiesPermission;
import org.flowerplatform.web.security.permission.PermissionDescriptor;
import org.flowerplatform.web.security.sandbox.FlowerWebPolicy;
import org.flowerplatform.web.security.sandbox.SecurityEntityAdaptor;
import org.flowerplatform.web.security.sandbox.SecurityUtils;
import org.hibernate.Query;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Service used to make CRUD operations on <code>Permission</code> entity.
*
* @see BootstrapService#initialize()
* @see ServiceRegistry
*
* @author Cristi
* @author Cristina
* @author Mariana
*
*/
public class PermissionService {
public static final String SERVICE_ID = "permissionService";
private static final Logger logger = LoggerFactory.getLogger(PermissionService.class);
private static final String SHOW_ALL_APPLICABLE_PERMISSIONS_PER_FILTERED_RESOURCE = "users.permissions.showAllApplicablePermissionsPerFilteredResource";
static {
CommonPlugin.getInstance().getFlowerProperties().addProperty(new AddBooleanProperty(SHOW_ALL_APPLICABLE_PERMISSIONS_PER_FILTERED_RESOURCE, "true"));
}
public static PermissionService getInstance() {
return (PermissionService) CommunicationPlugin.getInstance().getServiceRegistry().getService(SERVICE_ID);
}
/**
* Converts a {@link Group} to {@link PermissionAdminUIDto}.
*
* @see #findAllAsAdminUIDto()
* @see #findByIdAsAdminUIDto()
*
*/
private PermissionAdminUIDto convertPermissionToPermissionAdminUIDto(PermissionEntity permission) {
PermissionAdminUIDto dto = new PermissionAdminUIDto();
dto.setId(permission.getId());
dto.setName(permission.getName());
dto.setActions(permission.getActions());
dto.setAssignedTo(permission.getAssignedTo());
dto.setType(permission.getType());
dto.setIsEditable(true);
return dto;
}
/**
* Finds the {@link PermissionEntity permission} given by its id and returns a {@link PermissionAdminUIDto}.
*
*/
public PermissionAdminUIDto findByIdAsAdminUIDto(final long id) {
logger.debug("Find permission with id = {}", id);
DatabaseOperationWrapper wrapper = new DatabaseOperationWrapper(new DatabaseOperation() {
@Override
public void run() {
PermissionEntity permission = wrapper.find(PermissionEntity.class, id);
if (permission == null)
throw new RuntimeException(String.format("Permission with id=%s was not found in the DB.", id));
wrapper.setOperationResult(convertPermissionToPermissionAdminUIDto(permission));
}
});
return (PermissionAdminUIDto) wrapper.getOperationResult();
}
/**
* Finds all {@link PermissionEntity}s and returns a list of their corresponding {@link PermissionAdminUIDto}.
*
*/
public List<PermissionAdminUIDto> findAllAsAdminUIDto() {
logger.debug("Find all permissions");
final List<PermissionAdminUIDto> list = new ArrayList<PermissionAdminUIDto>();
new DatabaseOperationWrapper(new DatabaseOperation() {
@Override
public void run() {
for (PermissionEntity permission : wrapper.findAll(PermissionEntity.class)) {
try {
SecurityUtils.checkModifyTreePermission(permission);
if (Class.forName(permission.getType()).equals(AdminSecurityEntitiesPermission.class))
SecurityUtils.checkCurrentUserIsAdmin(null);
list.add(convertPermissionToPermissionAdminUIDto(permission));
} catch (Exception e) {}
}
}
});
return list;
}
/**
* Finds {@link PermissionEntity}s that correspond to given {@link PermissionsByResourceFilter}
* and returns a list of their corresponding {@link PermissionAdminUIDto}.
*
*/
@SuppressWarnings("unchecked")
public List<PermissionAdminUIDto> findAsAdminUIDtoFilterByResource(PermissionsByResourceFilter resourceFilter) {
if (resourceFilter == null) {
return findAllAsAdminUIDto();
}
final List<String> patterns = new ArrayList<String>();
patterns.add(resourceFilter.getResource());
// add filter by root/dir/* for root/dir
if (!resourceFilter.getResource().endsWith("/*"))
patterns.add(resourceFilter.getResource() + (resourceFilter.getResource().endsWith("/") ? "*" : "/*"));
// add filter by *
patterns.add("*");
int fromIndex = 0;
int index = resourceFilter.getResource().indexOf("/", fromIndex);
while (index != -1) {
patterns.add(resourceFilter.getResource().substring(0, index) + "/*");
fromIndex = index + 1;
index = resourceFilter.getResource().indexOf("/", fromIndex);
}
final List<PermissionAdminUIDto> listDtos = new ArrayList<PermissionAdminUIDto>();
new DatabaseOperationWrapper(new DatabaseOperation() {
@Override
public void run() {
List<PermissionEntity> permissionEntities;
Query q = wrapper.createQuery("SELECT e FROM PermissionEntity e WHERE e.name in :names ORDER by e.type, e.name");
q.setParameterList("names", patterns);
q.setReadOnly(true);
permissionEntities = q.list();
boolean showAllApplicablePermissions = Boolean.valueOf(CommonPlugin.getInstance().getFlowerProperties().getProperty(SHOW_ALL_APPLICABLE_PERMISSIONS_PER_FILTERED_RESOURCE));
for (PermissionEntity permission : permissionEntities) {
boolean isEditable = false;
try {
SecurityUtils.checkModifyTreePermission(permission);
// permission check did not throw a security exception => permission is editable by the current user
isEditable = true;
} catch (SecurityException e) {
// do nothing
}
// return this permission if it is editable OR if all applicable permissions must be displayed (even if they are not editable by the current user)
if (isEditable || showAllApplicablePermissions) {
PermissionAdminUIDto dto = convertPermissionToPermissionAdminUIDto(permission);
dto.setIsEditable(isEditable);
listDtos.add(dto);
}
}
}
});
return listDtos;
}
/**
* Validates the information found in {@link PermissionAdminUIDto dto} and
* creates/updates the {@link PermissionEntity}.
*
* <p>
* The validation with return messages to client if data is invalid.
*
* <p>
* Also return a special value in the map if the permission is a tree permission
* and the resource is a folder. The client will then ask the user if similar
* permissions (e.g. for root/org1 -> root/org1/*) should be modified as well.
*
* @see #validDto()
*
*/
@SuppressWarnings("unchecked")
public Map<String, String> mergeAdminUIDto(ServiceInvocationContext context, final PermissionAdminUIDto dto) {
logger.debug("Merge permission = {}", dto.getName());
DatabaseOperationWrapper wrapper = new DatabaseOperationWrapper(new DatabaseOperation() {
@Override
public void run() {
PermissionEntity permissionEntity;
if (dto.getId() == 0) {
permissionEntity = EntityFactory.eINSTANCE.createPermissionEntity();
} else {
permissionEntity = wrapper.find(PermissionEntity.class, (long)dto.getId());
SecurityUtils.checkModifyTreePermission(permissionEntity);
}
try {
if (Class.forName(dto.getType()).equals(AdminSecurityEntitiesPermission.class)) {
SecurityUtils.checkCurrentUserIsAdmin(String.format("Current user can not modify %s because he is not admin.", AdminSecurityEntitiesPermission.class.getSimpleName()));
}
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
permissionEntity.setName(dto.getName());
permissionEntity.setAssignedTo(dto.getAssignedTo());
permissionEntity.setActions(dto.getActions());
permissionEntity.setType(dto.getType());
PermissionDescriptor descriptor = ((FlowerWebPolicy)Policy.getPolicy()).getPermissionDescriptor(permissionEntity.getType());
Map<String, String> validationResults = descriptor.validate(createPermission(permissionEntity));
String assignedTo = permissionEntity.getAssignedTo();
if (!assignedTo.equals(PermissionEntity.ANY_ENTITY)) {
String message = SecurityUtils.validateSecurityEntity(assignedTo);
if (message != null) {
validationResults.put(PermissionDescriptor.ASSIGNED_TO_FIELD, message);
}
}
if (!validationResults.isEmpty()) {
wrapper.setOperationResult(validationResults);
return;
}
SecurityUtils.checkModifyTreePermission(permissionEntity);
Map<String, String> rslt = new HashMap<String, String>();
// check if tree permission on folder
if (descriptor.isTreePermission()) {
if (isFolder(permissionEntity.getName())) {
rslt.put("modifySimilarPermission", "");
}
}
wrapper.merge(permissionEntity);
wrapper.setOperationResult(rslt);
}
});
return (Map<String, String>) wrapper.getOperationResult();
}
/**
* Deletes all {@link PermissionEntity}s based on the list of their ids.
*
* <p>
* Returns a list of boolean values: for each deleted permission, return
* <code>true</code> if the permission is a tree permission and the resource
* is a folder. The client will then ask the user if similar permissions
* (e.g. for root/org1 -> root/org1/*) should be deleted as well.
*
*
*/
public List<Boolean> delete(final List<Integer> ids) {
final List<Boolean> result = new ArrayList<Boolean>();
new DatabaseOperationWrapper(new DatabaseOperation() {
@Override
public void run() {
for (Integer id : ids) {
PermissionEntity permissionEntity = wrapper.find(PermissionEntity.class, Long.valueOf(id));
SecurityUtils.checkModifyTreePermission(permissionEntity);
try {
if (Class.forName(permissionEntity.getType()).equals(AdminSecurityEntitiesPermission.class)) {
SecurityUtils.checkCurrentUserIsAdmin(String.format("Current user can not delete %s because he is not admin.", AdminSecurityEntitiesPermission.class.getSimpleName()));
}
PermissionDescriptor descriptor = ((FlowerWebPolicy)Policy.getPolicy()).getPermissionDescriptor(permissionEntity.getType());
if (descriptor.isTreePermission()) {
result.add(isFolder(permissionEntity.getName()));
}
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
logger.debug("Delete {}", permissionEntity);
wrapper.delete(permissionEntity);
}
}
});
return result;
}
private boolean isFolder(String path) {
File file = new File(CommonPlugin.getInstance().getWorkspaceRoot(), path.split("/\\*")[0]);
return file.isDirectory() && file.exists();
//
// path = path.split("/\\*")[0];
// IResource resource;
// try {
// resource = ResourcesPlugin.getWorkspace().getRoot().getFolder(new Path(path));
// } catch (IllegalArgumentException e) {
// return false;
// }
// return resource.exists();
}
@SuppressWarnings("unchecked")
public List<PermissionEntity> findPermissionsByType(final String type) {
DatabaseOperationWrapper wrapper = new DatabaseOperationWrapper(new DatabaseOperation() {
@Override
public void run() {
Query q = wrapper.createQuery("SELECT p FROM PermissionEntity p WHERE p.type = :type ORDER BY p.name");
q.setParameter("type", type);
wrapper.setOperationResult(q.list());
}
});
return (List<PermissionEntity>) wrapper.getOperationResult();
}
public void onSecurityEntityDelete(final ISecurityEntity securityEntity) {
new DatabaseOperationWrapper(new DatabaseOperation() {
@Override
public void run() {
// delete permissions assigned to deleted entity
String assignedTo = SecurityEntityAdaptor.getAssignedTo(securityEntity);
Query query = wrapper.createQuery("DELETE FROM PermissionEntity p WHERE p.assignedTo = :assigned_to");
query.setParameter("assigned_to", assignedTo);
query.executeUpdate();
// delete/edit permissions where actions contain deleted entity
String actions = SecurityEntityAdaptor.getAssignedTo(securityEntity);
Query q = wrapper.createQuery("SELECT p FROM PermissionEntity p WHERE p.actions LIKE :actions");
q.setParameter("actions", "%" + actions + "%");
@SuppressWarnings("unchecked")
List<PermissionEntity> list = q.list();
for (PermissionEntity permission : list) {
if (permission.getActions().equals(actions)) {
// delete permission
wrapper.delete(permission);
} else {
// update actions
List<ISecurityEntity> assignableEntities = SecurityEntityAdaptor.csvStringToSecurityEntityList(permission.getActions(), true);
for (Iterator<ISecurityEntity> it = assignableEntities.iterator(); it.hasNext();) {
Entity entity = (Entity) it.next();
if (entity != null) {
if (entity.getId() == ((Entity) securityEntity).getId()) {
it.remove();
break;
}
} else {
it.remove();
}
}
Set<String> names = new HashSet<String>();
for (ISecurityEntity entity : assignableEntities) {
names.add(SecurityEntityAdaptor.getAssignedTo(entity));
}
permission.setActions(SecurityEntityAdaptor.toCsvString(names));
wrapper.merge(permission);
}
}
}
});
}
public void onSecurityEntityUpdate(final ISecurityEntity initialEntity, final ISecurityEntity newEntity) {
new DatabaseOperationWrapper(new DatabaseOperation() {
@Override
public void run() {
String initialAssignedTo = SecurityEntityAdaptor.getAssignedTo(initialEntity);
String newAssignedTo = SecurityEntityAdaptor.getAssignedTo(newEntity);
if (!initialAssignedTo.equals(newAssignedTo)) {
// update permissions assigned to entity
Query query = wrapper.createQuery("UPDATE PermissionEntity " +
"SET assignedTo = :newAssignedTo " +
"WHERE assignedTo = :initialAssignedTo");
query.setParameter("newAssignedTo", newAssignedTo);
query.setParameter("initialAssignedTo", initialAssignedTo);
query.executeUpdate();
// update permissions where actions contain entity
Query q = wrapper.createQuery("SELECT p FROM PermissionEntity p WHERE p.actions LIKE :actions");
q.setParameter("actions", "%" + initialAssignedTo + "%");
@SuppressWarnings("unchecked")
List<PermissionEntity> list = q.list();
for (PermissionEntity permission : list) {
permission.setActions(permission.getActions().replaceAll(Matcher.quoteReplacement(initialAssignedTo), Matcher.quoteReplacement(newAssignedTo)));
wrapper.merge(permission);
}
}
}
});
}
public List<PermissionDescriptor> getPermissionDescriptors() {
FlowerWebPolicy policy = (FlowerWebPolicy) Policy.getPolicy();
return policy.getPermissionDescriptors();
}
/**
* Instantiates a {@link Permission} from the record saved in the database,
* i.e. in the {@link org.flowerplatform.web.entity.PermissionEntity}.
*
*/
@SuppressWarnings("unchecked")
public Permission createPermission(org.flowerplatform.web.entity.PermissionEntity permission) {
Permission instance = null;
try {
Class<? extends Permission> permissionClass = (Class<? extends Permission>) Class.forName(permission.getType());
Constructor<? extends Permission> constructor = permissionClass.getConstructor(String.class, String.class);
instance = constructor.newInstance(permission.getName(), permission.getActions());
} catch (Exception e) {
throw new RuntimeException(e);
}
return instance;
}
private Observer securityEntityObserver = new Observer() {
@Override
public void update(Observable o, Object arg) {
List<?> list = (List<?>) arg;
if (list.get(0).equals(ServiceObservable.DELETE) && list.get(1) instanceof ISecurityEntity) {
ISecurityEntity securityEntity = (ISecurityEntity) list.get(1);
onSecurityEntityDelete(securityEntity);
} else {
if (list.get(0).equals(ServiceObservable.UPDATE) && list.get(1) instanceof ISecurityEntity && list.get(2) instanceof ISecurityEntity) {
onSecurityEntityUpdate((ISecurityEntity) list.get(1), (ISecurityEntity) list.get(2));
}
}
}
};
public Observer getSecurityEntityObserver() {
return securityEntityObserver;
}
}