/* 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.sandbox;
import java.io.File;
import java.security.AccessController;
import java.security.CodeSource;
import java.security.Permission;
import java.security.PermissionCollection;
import java.security.Permissions;
import java.security.Policy;
import java.security.PrivilegedAction;
import java.security.ProtectionDomain;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.eclipse.osgi.internal.permadmin.BundlePermissions;
import org.flowerplatform.common.CommonPlugin;
import org.flowerplatform.web.database.DatabaseOperation;
import org.flowerplatform.web.database.DatabaseOperationWrapper;
import org.flowerplatform.web.security.permission.AbstractTreePermission;
import org.flowerplatform.web.security.permission.AdminSecurityEntitiesPermissionDescriptor;
import org.flowerplatform.web.security.permission.FlowerWebFilePermission;
import org.flowerplatform.web.security.permission.FlowerWebFilePermissionDescriptor;
import org.flowerplatform.web.security.permission.ModifyTreePermissionsPermissionDescriptor;
import org.flowerplatform.web.security.permission.PermissionDescriptor;
import org.flowerplatform.web.security.service.PermissionService;
import org.hibernate.Query;
import org.flowerplatform.web.entity.ISecurityEntity;
import org.flowerplatform.web.entity.PermissionEntity;
import org.flowerplatform.web.entity.User;
/**
* This is our custom security policy responsible to check if permissions are allowed
* to the current principal(s), by using the database and the permission caches that it maintains.
*
* @author Cristi
* @author Florin
*
*
*/
@SuppressWarnings("restriction")
public class FlowerWebPolicy extends Policy {
/**
*
*/
private Policy defaultPolicy;
/**
* The descriptors of the permissions that this policy enforces.
*
* @see #addPermissionDescriptor()
*
*/
private PermissionDescriptorsTable permissionDescriptors = new PermissionDescriptorsTable();
/**
* Cache for tree permissions.
* The key of this map is the implemented permission type as defined by the permission descriptor.
*
* <p>
* For a given permission (implementation) type, this cache holds the permissions for ALL users, groups, organizations.
*
* @see #getTreePermissions()
*
*/
protected Map<Class<? extends AbstractTreePermission>, TreePermissionCollection> treePermissionsCache = Collections.synchronizedMap(new HashMap<Class<? extends AbstractTreePermission>, TreePermissionCollection>());
/**
* Cache for normal (not tree) permissions.
* The key is the user id. The value is a {@link PermissionCollection}, that contains
* all permissions for this user, the groups of the user and organizations of the user.
*
* @see #getPermissionsForUser()
* @see #clearUserPermissionsCache()
*
*
*/
protected Map<Long, PermissionCollection> normalPermissionsCache = Collections.synchronizedMap(new HashMap<Long, PermissionCollection>());
private List<IFlowerWebPolicyExtension> extensions;
/**
* Installs the permission descriptors for the built in permission types:
*
* <ul>
* <li>{@link FlowerWebFilePermission}
* </ul>
*
*
*/
public FlowerWebPolicy(Policy defaultPolicy) {
this.defaultPolicy = defaultPolicy;
addPermissionDescriptor(new AdminSecurityEntitiesPermissionDescriptor());
addPermissionDescriptor(new FlowerWebFilePermissionDescriptor());
addPermissionDescriptor(new ModifyTreePermissionsPermissionDescriptor());
}
public List<IFlowerWebPolicyExtension> getExtensions() {
if (extensions == null) {
extensions = new ArrayList<IFlowerWebPolicyExtension>();
}
return extensions;
}
public void setExtensions(List<IFlowerWebPolicyExtension> extensions) {
this.extensions = extensions;
}
/**
* Adds a new permission descriptor.
*
* @see #permissionDescriptors
*
*
*/
public void addPermissionDescriptor(PermissionDescriptor descriptor) {
permissionDescriptors.put(descriptor);
}
/**
* Returns an empty collection because we "disabled" this method.
* In theory, only {@link #implies()} of this class is supposed to call this method.
*
*
*/
public PermissionCollection getPermissions(ProtectionDomain domain) {
return new Permissions();
}
/**
* Method introduced because connecting JVisualVM through jmx would not work due
* to permission problems.
* It seemed that when using the default PolicyFile it returned a modifiable {@link PermissionCollection}
* along with some permisions in it. I think it is safe to return a modifiable {@link PermissionCollection}
* without any permissions inside.
*
* @author Sorin
*/
@Override
public PermissionCollection getPermissions(CodeSource codesource) {
return new Permissions();
}
/**
* Permission check main logic. Uses the caches (that are populated from the DB) to see
* if the permission is granted to the current user (or principal).
*
*
*/
public boolean implies(final ProtectionDomain domain, final Permission permission) {
if (permission.getClass().getName().equals("javax.security.auth.AuthPermission") && permission.getName().equals("doAsPrivileged")) {
// coding like this to avoid ClassCircularityError
// allow this permission for everyone
return true;
}
String flowerWebPolicy$CallPrivilegedImpliesAction_className = FlowerWebPolicy.class.getName() + "$CallPrivilegedImpliesAction";
String flowerWebPolicy$CallPrivilegedImpliesAction_classFile = flowerWebPolicy$CallPrivilegedImpliesAction_className.replace(".", File.separator) + ".class";
if (permission.getName().contains(flowerWebPolicy$CallPrivilegedImpliesAction_classFile)
&& permission.getActions().equals("read")) {
// The code below tries to read CallPrivilegedImpliesAction class, which triggers a security check (read permission for the class file).
// As a result this method is called and the code below tries to read CallPrivilegedImpliesAction, .... and so a ClassCircularityError occurs.
// So we allow this permission to avoid this error.
return true;
}
if (permission.getClass().getName().equals("java.util.PropertyPermission") &&
permission.getName().equals("user.dir") &&
permission.getActions().equals("read")) {
// allow user.dir permission for everyone
return true;
}
// AccessController.doPrivileged means that a new access control context
// will be created that will have a single bundle protection domain (the
// one for *.mp.web, that has all privileges).
// This is because we need to have all permissions in order to be able
// to decide if a permission is allowed. For example,
// File.getAbsolutePath, called in
// TreePermissionCollection will require access to read the system
// property user.dir.
// The protection domains in the access control context might or might
// have not this permission. So we make a privileged call
// to ensure that required permission is allowed.
return AccessController.doPrivileged(new CallPrivilegedImpliesAction(domain, permission), null); // call to privilegedImplies
}
/**
*
*/
@SuppressWarnings({ "unchecked" })
private boolean privilegedImplies(ProtectionDomain domain, Permission permission) {
boolean implies = false;
PermissionDescriptor descriptor = permissionDescriptors.getByHandledPermissionType(permission.getClass());
TreePermissionCollection treePermissionCollection = null;
if (domain.getPrincipals().length > 0 && descriptor != null && descriptor.isTreePermission()) {
treePermissionCollection = getTreePermissions((Class<? extends AbstractTreePermission>) descriptor.getImplementedPermissionType());
}
if (domain.getPrincipals().length > 0 && treePermissionCollection != null && treePermissionCollection.belongsToRoot(permission.getName())) {
// when domain has a principal and requests a permission from eclipse workspace, we handle this by verifying our
// database permissions
FlowerWebPrincipal principal = (FlowerWebPrincipal) domain.getPrincipals()[0];
implies = treePermissionCollection.implies((FlowerWebPrincipal) principal, permission);
if (!implies) {
// if this policy denies permission, an exception is thrown to stop ProtectionDomain to check its permissions
// if this would return false, ProtectionDomain would ask its BundlesPermissions (osgi permissions table)
throw new FlowerWebSecurityException(String.format("Permission denied for %s", permission));
}
} else if (domain.getPrincipals().length > 0 && descriptor != null && !descriptor.isTreePermission()) {
FlowerWebPrincipal principal = (FlowerWebPrincipal) domain.getPrincipals()[0];
PermissionCollection permissions = getNormalPermissions(principal.getUser());
implies = permissions.implies(permission);
if (!implies) {
throw new FlowerWebSecurityException(String.format("Permission denied for %s", permission));
}
} else if (domain.getPermissions() instanceof BundlePermissions) {
// let BundlePermissions from ProtectionDomain decide
// if osgi security table is empty (currently it is), all permissions will be allowed
// see SecurityAdmin.checkPermission() and SecurityAdmin.DEFAULT_DEFAULT;
implies = false;
} else /*if (domain.getPrincipals().length == 0)*/ {
// There will be some occasions when the domain it is not the domain of an osgi bundle.
// This is true for web server code (lib/catalina.jar, bin/bootstrap.jar etc.),
// for org.eclipse.osgi_3.5.2.R35x_v20100126.jar, for org.flowerplatform.web.app project.
// This means we allow the requested permission (as all.policy gives all permission to everyone)
implies = defaultPolicy.implies(domain, permission);
}
return implies;
}
/**
* The path (a.k.a resource, name) of a tree permission is relative to the workspace.
*
*
*/
public File getRuntimeWorkspace() {
return CommonPlugin.getInstance().getWorkspaceRoot();
}
/**
* Returns the tree permission cache for the specified type.
* If the cache is empty, it populates it from the DB.
*
*
*/
private TreePermissionCollection getTreePermissions(Class<? extends AbstractTreePermission> treePermissionType) {
PermissionService permissionService = PermissionService.getInstance();
TreePermissionCollection treePermissionCollection = treePermissionsCache.get(treePermissionType);
if (treePermissionCollection == null) {
// permissions are ordered by path
List<org.flowerplatform.web.entity.PermissionEntity> permissions = permissionService.findPermissionsByType(treePermissionType.getName());
treePermissionCollection = new TreePermissionCollection(treePermissionType, getRuntimeWorkspace());
for (org.flowerplatform.web.entity.PermissionEntity permissionEntity : permissions) {
// use eager loading for the entity's associations because we're keeping the entity in the cache
ISecurityEntity securityEntity = SecurityEntityAdaptor.toSecurityEntity(permissionEntity.getAssignedTo(), true);
AbstractTreePermission treePermission = (AbstractTreePermission) permissionService.createPermission(permissionEntity);
treePermissionCollection.addPermission(securityEntity, treePermission);
}
treePermissionsCache.put(treePermissionType, treePermissionCollection);
}
return treePermissionCollection;
}
/**
* Returns all the permissions that are enforced for the user (the permissions for the user,
* the permissions for the user's group and the permissions for the user's organizations).
*
* @author Cristi
* @author Florin
* @autor Mariana
*
*
*/
PermissionCollection getNormalPermissions(final User user) {
if (normalPermissionsCache.get(user.getId()) == null) {
final Permissions permissions = new Permissions();
new DatabaseOperationWrapper(new DatabaseOperation() {
@Override
public void run() {
Query q = wrapper.createQuery("SELECT p " +
"FROM PermissionEntity p " +
"WHERE p.assignedTo = :assigned_to " +
"OR p.assignedTo = '@ALL'" +
"OR EXISTS (SELECT g.name FROM User u JOIN u.groupUsers gu JOIN gu.group g WHERE u.login = :login AND p.assignedTo = CONCAT('@', g.name)) "+
"OR EXISTS (SELECT o2.name FROM User u2 JOIN u2.organizationUsers ou2 JOIN ou2.organization o2 WHERE u2.login = :login AND p.assignedTo = CONCAT('#', o2.name)) ");
q.setParameter("assigned_to", '$' + user.getLogin());
q.setParameter("login", user.getLogin());
@SuppressWarnings("unchecked")
List<PermissionEntity> resultList = q.list();
PermissionService permissionService = (PermissionService) PermissionService.getInstance();
for (PermissionEntity entity: resultList) {
PermissionDescriptor descriptor = getPermissionDescriptor(entity.getType());
if (!descriptor.isTreePermission()) {
permissions.add(permissionService.createPermission(entity));
}
}
normalPermissionsCache.put(user.getId(), permissions);
}
});
}
return normalPermissionsCache.get(user.getId());
}
/**
* If <code>permissionEntity</code> is of tree type, this method will clear
* the entry for the implemented permission type (as defined by IPermissionDescriptor)
* from {@link #treePermissionsCache}. If <code>permissionEntity</code> is a normal
* permission, this method will clear {@link #normalPermissionsCache}.
*
*/
public void updateCachesFor(PermissionEntity permissionEntity) {
PermissionDescriptor descriptor = getPermissionDescriptor(permissionEntity.getType());
if (descriptor.isTreePermission()) {
treePermissionsCache.remove(descriptor.getImplementedPermissionType());
} else {
normalPermissionsCache.clear();
}
}
/**
* Caches are cleared.
*
*/
public void updateCachesFor(ISecurityEntity securityEntity) {
// FlowerWebPolicy.treePermissionsCache must be updated because the entries in the TreePermissionCollection have an ISecurityEntity.
treePermissionsCache.clear();
// FlowerWebPolicy.normalPermissionsCache must be updated when user, group or organization changes (because user might not belong anymore to group/organization)
if (securityEntity instanceof User) {
User user = (User) securityEntity;
normalPermissionsCache.remove(user.getId());
} else {
normalPermissionsCache.clear();
}
}
/**
*
*/
public List<PermissionDescriptor> getPermissionDescriptors() {
return permissionDescriptors.getPermissionDescriptors();
}
/**
*
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
public PermissionDescriptor getPermissionDescriptor(String permissionEntityType) {
Class clazz;
try {
clazz = Class.forName(permissionEntityType);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
return permissionDescriptors.getByImplementedPermissionType(clazz);
}
/**
* This calls the privilegedImplies method.
*
*/
class CallPrivilegedImpliesAction implements PrivilegedAction<Boolean> {
private ProtectionDomain domain;
private Permission permission;
public CallPrivilegedImpliesAction(ProtectionDomain domain, Permission permission) {
this.domain = domain;
this.permission = permission;
}
@Override
public Boolean run() {
return privilegedImplies(domain, permission);
}
}
}