/*
* Weblounge: Web Content Management System
* Copyright (c) 2003 - 2011 The Weblounge Team
* http://entwinemedia.com/weblounge
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package ch.entwine.weblounge.common.impl.security;
import ch.entwine.weblounge.common.impl.util.xml.XPathHelper;
import ch.entwine.weblounge.common.security.Authority;
import ch.entwine.weblounge.common.security.Permission;
import ch.entwine.weblounge.common.security.PermissionSet;
import ch.entwine.weblounge.common.security.User;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import javax.xml.xpath.XPath;
/**
* This class models the security constraints that apply to an arbitrary object
* in the system.
* <p>
* A security context definition contains information on permissions and roles
* or users that are needed to obtain them. The context usually looks like
* follows:
*
* <pre>
* <security>
* <owner>tobias.wunden</owner>
* <permission id="system:publish" type="role">system:publisher</permission>
* <permission id="system:write" type="user">tobias.wunden</permission>
* </security>
* </pre>
*/
public class SecurityContextImpl extends AbstractSecurityContext implements Cloneable {
/** Logging facility */
private static final Logger logger = LoggerFactory.getLogger(SecurityContextImpl.class);
/** Allowed authorizations */
private Map<Permission, Set<Authority>> context = null;
/** Allowed default authorizations */
private Map<Permission, Set<Authority>> defaultContext = null;
/** The permissions */
private Permission[] permissions = null;
/**
* Creates a default restriction set with no restrictions and a context
* identifier of <tt><default></tt>.
*/
public SecurityContextImpl() {
this(null);
}
/**
* Creates a default restriction set with the given name and initially no
* restrictions.
*
* @param owner
* the secured object owner
*/
public SecurityContextImpl(User owner) {
super(owner);
context = new HashMap<Permission, Set<Authority>>();
defaultContext = new HashMap<Permission, Set<Authority>>();
}
/**
* Adds <code>authority</code> to the authorized authorities regarding the
* given permission.
* <p>
* <b>Note:</b> Calling this method replaces any default authorities on the
* given permission, so if you want to keep them, add them here explicitly.
*
* @param permission
* the permission
* @param authority
* the item that is allowed to obtain the permission
*/
public void allow(Permission permission, Authority authority) {
if (permission == null)
throw new IllegalArgumentException("Permission cannot be null");
if (authority == null)
throw new IllegalArgumentException("Authority cannot be null");
logger.debug("Security context '{}' requires '{}' for permission '{}'", new Object[] {
this,
authority,
permission });
Set<Authority> a = context.get(permission);
if (a == null) {
a = new HashSet<Authority>();
context.put(permission, a);
permissions = null;
}
a.add(authority);
defaultContext.remove(permission);
}
/**
* Adds <code>authority</code> to the default authorized authorities regarding
* the given permission. Default authorities will not be stored in the
* database, thus saving lots of space and speeding things up.
*
* @param permission
* the permission
* @param authority
* the item that is allowed to obtain the permission
*/
public void allowDefault(Permission permission, Authority authority) {
if (permission == null)
throw new IllegalArgumentException("Permission cannot be null");
if (authority == null)
throw new IllegalArgumentException("Authority cannot be null");
logger.debug("Security context '{}' requires '{}' for permission '{}'", new Object[] {
this,
authority,
permission });
Set<Authority> a = defaultContext.get(permission);
if (a == null) {
a = new HashSet<Authority>();
defaultContext.put(permission, a);
permissions = null;
}
a.add(authority);
}
/**
* Removes <code>authority</code> from the denied authorities regarding the
* given permission. This method will remove the authority from both the
* explicitly allowed and the default authorities.
*
* @param permission
* the permission
* @param authority
* the authorization to deny
*/
public void deny(Permission permission, Authority authority) {
if (permission == null)
throw new IllegalArgumentException("Permission cannot be null");
if (authority == null)
throw new IllegalArgumentException("Authority cannot be null");
logger.debug("Security context '{}' requires '{}' for permission '{}'", new Object[] {
this,
authority,
permission });
deny(permission, authority, context);
deny(permission, authority, defaultContext);
}
/**
* Removes <code>authority</code> from the denied authorities found in
* <code>context</code> regarding the given permission.
*
* @param permission
* the permission
* @param authority
* the authorization to deny
* @param context
* the authorities context
*/
private void deny(Permission permission, Authority authority,
Map<Permission, Set<Authority>> context) {
Set<Authority> authorities = context.get(permission);
// If the authorities have been found, iterate over them to find a matching
// authority. We have to do this instead of directly calling
// authorities.remove(authority)
// because the context may contain AuthorityImpl instances which will equal
// a matching role (after casting them to an authority) but not v. v.
if (authorities != null) {
for (Authority a : authorities) {
if (a.isAuthorizedBy(authority)) {
authorities.remove(a);
return;
}
}
if (authorities.size() == 0) {
context.remove(permission);
}
}
}
/**
* Denies everyone and everything regarding permission <code>permission</code>
* .
*
* @param permission
* the permission
*/
public void denyAll(Permission permission) {
denyAll(permission, context);
denyAll(permission, defaultContext);
}
/**
* Denies everyone and everything regarding permission <code>permission</code>
* in the specified context.
*
* @param permission
* the permission
* @param context
* the context
*/
private void denyAll(Permission permission,
Map<Permission, Set<Authority>> context) {
Set<Authority> authorities = context.get(permission);
if (authorities != null) {
authorities.clear();
}
}
/**
* Denies everyone and everything.
*/
public void denyAll() {
context.clear();
defaultContext.clear();
}
/**
* Checks whether the roles that the caller currently owns satisfy the
* constraints of this context ion the given permission.
*
* @param permission
* the permission to obtain
* @param authority
* the object claiming the permission
* @return <code>true</code> if the item may obtain the permission
*/
public boolean check(Permission permission, Authority authority) {
if (permission == null)
throw new IllegalArgumentException("Permission cannot be null");
if (authority == null)
throw new IllegalArgumentException("Authority cannot be null");
logger.debug("Request to check permission '{}' for authority '{}' at {}", new Object[] {
permission,
authority,
this });
return check(permission, authority, defaultContext) || check(permission, authority, context);
}
/**
* Checks whether the roles that the caller currently owns satisfy the
* constraints of the given context regarding the given permission.
*
* @param permission
* the permission to obtain
* @param authority
* the object claiming the permission
* @param context
* the context
* @return <code>true</code> if the item may obtain the permission
*/
private boolean check(Permission permission, Authority authority,
Map<Permission, Set<Authority>> context) {
Set<Authority> authorities = context.get(permission);
if (authorities != null) {
for (Authority a : authorities) {
if (authority.isAuthorizedBy(a))
return true;
}
}
return false;
}
/**
* Returns <code>true</code> if the object <code>o</code> is allowed to act on
* the secured object in a way that satisfies the given permissionset
* <code>p</code>.
*
* @param permissions
* the required set of permissions
* @param authority
* the object claiming the permissions
* @return <code>true</code> if the object may obtain the permissions
*/
public boolean check(PermissionSet permissions, Authority authority) {
if (permissions == null)
throw new IllegalArgumentException("Permissions cannot be null");
if (authority == null)
throw new IllegalArgumentException("Authority cannot be null");
logger.debug("Request to check permissionset for authorization '{}' at {}", authority, this);
return checkOneOf(permissions, authority) && checkAllOf(permissions, authority);
}
/**
* Returns the authorities that are explicitly allowed by the context.
*
* @see ch.entwine.weblounge.common.security.SecurityContext#getAllowed(ch.entwine.weblounge.common.security.Permission)
*/
public Authority[] getAllowed(Permission p) {
Set<Authority> authorities = defaultContext.get(p);
if (authorities == null) {
authorities = context.get(p);
}
if (authorities != null) {
Authority[] a = new Authority[authorities.size()];
return authorities.toArray(a);
} else {
return new Authority[] {};
}
}
/**
* Returns all authorities that are explicitly denied by this security
* context. Since this context only defines allowed items, the returned array
* will always be empty.
*
* @see ch.entwine.weblounge.common.security.SecurityContext#getDenied(ch.entwine.weblounge.common.security.Permission)
*/
public Authority[] getDenied(Permission p) {
return new Authority[] {};
}
/**
* Returns <code>true</code> if the authorization is sufficient to obtain the
* "oneof" permission set.
*
* @param p
* the permission set
* @param authorization
* the authorization to check
* @return <code>true</code> if the user has one of the permissions
*/
protected boolean checkOneOf(PermissionSet p, Authority authorization) {
Permission[] permissions = p.some();
for (int i = 0; i < permissions.length; i++) {
if (check(permissions[i], authorization)) {
return true;
}
}
return (permissions.length == 0);
}
/**
* Returns <code>true</code> if the authorization is sufficient to obtain the
* "allof" permission set.
*
* @param p
* the permission set
* @param authorization
* the authorization to check
* @return <code>true</code> if the user has all of the permissions
*/
protected boolean checkAllOf(PermissionSet p, Authority authorization) {
Permission[] permissions = p.all();
for (int i = 0; i < permissions.length; i++) {
if (!check(permissions[i], authorization)) {
return false;
}
}
return true;
}
/**
* Returns the permissions that are defined in this security context.
*
* @return the permissions
*/
public Permission[] permissions() {
if (permissions == null) {
permissions = new Permission[context.size() + defaultContext.size()];
List<Permission> permissionList = new ArrayList<Permission>();
permissionList.addAll(context.keySet());
permissionList.addAll(defaultContext.keySet());
permissionList.toArray(permissions);
}
return permissions;
}
/**
* Initializes this context from an xml node.
*
* @param context
* the security context node
* @param path
* the XPath object used to parse the configuration
*/
public void init(XPath path, Node context) {
this.context.clear();
permissions = null;
// Read permissions
NodeList permissions = XPathHelper.selectList(context, "/security/permission", path);
for (int i = 0; i < permissions.getLength(); i++) {
Node p = permissions.item(i);
String id = XPathHelper.valueOf(p, "@id", path);
Permission permission = new PermissionImpl(id);
// Authority name
String require = XPathHelper.valueOf(p, "text()", path);
if (require == null) {
continue;
}
// Authority type
String type = XPathHelper.valueOf(p, "@type", path);
// Check for multiple authorities
StringTokenizer tok = new StringTokenizer(require, " ,;");
while (tok.hasMoreTokens()) {
String authorityId = tok.nextToken();
Authority authority = new AuthorityImpl(resolveAuthorityTypeShortcut(type), authorityId);
allow(permission, authority);
}
}
}
/**
* Serializes this security context.
*
* @return the serialized form of this restriction set
*/
public String toXml() {
StringBuffer b = new StringBuffer();
b.append("<security>");
// Owner
if (owner != null) {
b.append("<owner>");
b.append((new UserImpl(owner)).toXml());
b.append("</owner>");
}
// Permissions
for (Permission p : context.keySet()) {
Map<String, Set<Authority>> authorities = groupByType(context.get(p));
for (Map.Entry<String, Set<Authority>> entry : authorities.entrySet()) {
String type = entry.getKey();
b.append("<permission id=\"");
b.append(p.getContext() + ":" + p.getIdentifier());
b.append("\" type=\"");
b.append(getAuthorityTypeShortcut(type));
b.append("\">");
boolean first = true;
for (Authority authority : entry.getValue()) {
if (!first) {
b.append(",");
}
b.append(authority.getAuthorityId());
first = false;
}
b.append("</permission>");
}
}
b.append("</security>");
return b.toString();
}
/**
* Returns the authorities grouped by their types.
*
* @param authorities
* the authorities hash set
* @return the grouped authorities
*/
private Map<String, Set<Authority>> groupByType(Set<Authority> authorities) {
Map<String, Set<Authority>> types = new HashMap<String, Set<Authority>>();
for (Authority a : authorities) {
Set<Authority> al = types.get(a.getAuthorityType());
if (al == null) {
al = new HashSet<Authority>();
types.put(a.getAuthorityType(), al);
}
al.add(a);
}
return types;
}
/**
* Returns a copy of this security context.
*
* @see java.lang.Object#clone()
*/
public Object clone() throws CloneNotSupportedException {
SecurityContextImpl ctxt = (SecurityContextImpl) super.clone();
ctxt.owner = owner;
ctxt.context.putAll(context);
ctxt.defaultContext.putAll(defaultContext);
ctxt.permissions = permissions;
ctxt.owner = owner;
return ctxt;
}
}