/*
* Copyright Aduna (http://www.aduna-software.com/) (c) 2009-2010.
*
* Licensed under the Aduna BSD-style license.
*/
package org.openrdf.sail.accesscontrol;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.Map.Entry;
import org.openrdf.cursor.Cursor;
import org.openrdf.cursor.EmptyCursor;
import org.openrdf.cursor.FilteringCursor;
import org.openrdf.model.Literal;
import org.openrdf.model.Resource;
import org.openrdf.model.Statement;
import org.openrdf.model.URI;
import org.openrdf.model.Value;
import org.openrdf.model.vocabulary.RDF;
import org.openrdf.model.vocabulary.XMLSchema;
import org.openrdf.query.BindingSet;
import org.openrdf.query.algebra.And;
import org.openrdf.query.algebra.Bound;
import org.openrdf.query.algebra.Compare;
import org.openrdf.query.algebra.Filter;
import org.openrdf.query.algebra.Join;
import org.openrdf.query.algebra.LeftJoin;
import org.openrdf.query.algebra.Not;
import org.openrdf.query.algebra.Or;
import org.openrdf.query.algebra.QueryModel;
import org.openrdf.query.algebra.QueryModelNode;
import org.openrdf.query.algebra.StatementPattern;
import org.openrdf.query.algebra.TupleExpr;
import org.openrdf.query.algebra.Union;
import org.openrdf.query.algebra.Var;
import org.openrdf.query.algebra.helpers.QueryModelVisitorBase;
import org.openrdf.sail.SailConnection;
import org.openrdf.sail.accesscontrol.vocabulary.ACL;
import org.openrdf.sail.helpers.SailConnectionWrapper;
import org.openrdf.store.Session;
import org.openrdf.store.SessionManager;
import org.openrdf.store.StoreException;
/**
* @author Jeen Broekstra
*/
public class AccessControlConnection extends SailConnectionWrapper {
private URI inheritanceProperty;
private List<URI> accessAttributes;
/**
* @param delegate
*/
public AccessControlConnection(SailConnection delegate) {
super(delegate);
}
/**
* Verifies if the supplied subject is editable by the current user.
*
* @param subject
* the subject for which edit permission is to be checked.
* @return true if the subject is editable by the current user, false if not.
* @throws StoreException
*/
public boolean isEditable(Resource subject)
throws StoreException
{
return hasPermissionOnSubject(getCurrentUser(), subject, ACL.EDIT);
}
/**
* Verifies if the supplied subject is viewable by the current user.
*
* @param subject
* the subject for which view permission is to be checked.
* @return true if the subject is viewable by the current user, false if not.
* @throws StoreException
*/
public boolean isViewable(Resource subject)
throws StoreException
{
return hasPermissionOnSubject(getCurrentUser(), subject, ACL.VIEW);
}
@Override
public void addStatement(Resource subj, URI pred, Value obj, Resource... contexts)
throws StoreException
{
if (isEditable(subj)) {
super.addStatement(subj, pred, obj, contexts);
}
else {
throw new StoreException("insufficient access rights on subject " + subj.stringValue());
}
}
@Override
public Cursor<? extends Statement> getStatements(Resource subj, URI pred, Value obj,
boolean includeInferred, Resource... contexts)
throws StoreException
{
if (subj != null) {
if (isViewable(subj)) {
return super.getStatements(subj, pred, obj, includeInferred, contexts);
}
else {
return EmptyCursor.getInstance();
}
}
else {
Cursor<? extends Statement> result = super.getStatements(subj, pred, obj, includeInferred, contexts);
return new FilteringCursor<Statement>(result) {
@Override
protected boolean accept(Statement st)
throws StoreException
{
return isViewable(st.getSubject());
}
};
}
}
@Override
public void removeStatements(Resource subj, URI pred, Value obj, Resource... contexts)
throws StoreException
{
if (subj != null) {
if (isEditable(subj)) {
super.removeStatements(subj, pred, obj, contexts);
}
else {
// TODO: ignore statements that aren't viewable?
// Remark Jeen: I would only do that in the case where the subject
// was not explicitly specified in the method call. In the current
// case, the user explicitly tried to perform a remove on a specific
// subject to which he has no access rights. IMHO, that should
// _always_ result in an error.
throw new StoreException("insufficient access rights on subject " + subj.stringValue());
}
}
else {
Cursor<? extends Statement> toBeRemovedStatements = super.getStatements(null, pred, obj, false,
contexts);
try {
Statement st;
while ((st = toBeRemovedStatements.next()) != null) {
Resource subject = st.getSubject();
if (isEditable(subject)) {
super.removeStatements(subject, pred, obj, contexts);
}
else if (isViewable(subject)) {
// Since the user did not explicitly specify the subject being
// removed, we silently ignore the statement if the subject is
// not viewable by the user.
throw new StoreException("insufficient access rights on subject " + subject.stringValue());
}
}
}
finally {
toBeRemovedStatements.close();
}
}
}
@Override
public Cursor<? extends BindingSet> evaluate(QueryModel query, BindingSet bindings, boolean includeInferred)
throws StoreException
{
// Clone the tuple expression to allow for more aggresive optimizations
query = query.clone();
query.visit(new AccessControlQueryExpander());
return super.evaluate(query, bindings, includeInferred);
}
@Override
public void commit()
throws StoreException
{
try {
super.commit();
}
finally {
resetCachedData();
}
}
@Override
public void close()
throws StoreException
{
try {
super.close();
}
finally {
resetCachedData();
}
}
private synchronized void resetCachedData() {
accessAttributes = null;
inheritanceProperty = null;
}
/**
* Retrieves the property value for supplied subject and predicate, if that
* value is a resource. Optionally, it uses the acl:InheritanceProperty to
* find a value for the supplied predicate on one of the subject's parents.
*
* @param subject
* the subject node for which to retrieve the property value as a
* Resource.
* @param predicate
* the property name for which to retrieve the value.
* @param inherit
* indicates if the subject's parent(s) should be checked for a
* property value if the subject has no valid value itself.
* @param contexts
* zero or more contexts in which to find the value.
* @return a property value as a resource, or null if no valid property value
* was found.
* @throws StoreException
*/
private Resource getPropertyResourceValue(Resource subject, URI predicate, boolean inherit,
Resource... contexts)
throws StoreException
{
return getPropertyResourceValue(subject, predicate, inherit, new ArrayList<Resource>(), contexts);
}
private Resource getPropertyResourceValue(Resource subject, URI predicate, boolean inherit,
final Collection<Resource> visited, Resource... contexts)
throws StoreException
{
// loop detection
if (visited.contains(subject)) {
return null;
}
Resource result = null;
Cursor<? extends Statement> statements = super.getStatements(subject, predicate, null, true, contexts);
try {
Statement st;
while ((st = statements.next()) != null) {
if (st.getObject() instanceof Resource) {
result = (Resource)st.getObject();
break;
}
}
}
finally {
statements.close();
}
// see if we should try and find a value from the supplied resource's
// parent.
if (result == null && inherit) {
visited.add(subject);
URI inheritanceProperty = getInheritanceProperty();
if (inheritanceProperty != null) {
Cursor<? extends Statement> parentStatements = super.getStatements(subject, inheritanceProperty,
null, true);
try {
Statement parentStatement;
while ((parentStatement = parentStatements.next()) != null) {
Value value = parentStatement.getObject();
if (value instanceof Resource) {
result = getPropertyResourceValue((Resource)value, predicate, false, visited, contexts);
if (result != null) {
break;
}
}
}
}
finally {
parentStatements.close();
}
}
}
return result;
}
private Set<Resource> getPropertyResourceValues(Resource subject, URI predicate, boolean inherit,
Resource... contexts)
throws StoreException
{
Set<Resource> results = new HashSet<Resource>();
getPropertyResourceValues(results, subject, predicate, inherit, new ArrayList<Resource>(), contexts);
return results;
}
private void getPropertyResourceValues(Set<Resource> results, Resource subject, URI predicate,
boolean inherit, final List<Resource> visited, Resource... contexts)
throws StoreException
{
// loop detection
if (visited.contains(subject)) {
return;
}
Cursor<? extends Statement> statements = super.getStatements(subject, predicate, null, true, contexts);
try {
Statement st;
while ((st = statements.next()) != null) {
if (st.getObject() instanceof Resource) {
results.add((Resource)st.getObject());
}
}
}
finally {
statements.close();
}
// see if we should try and find a value from the supplied resource's
// parent.
if (inherit) {
visited.add(subject);
URI inheritanceProperty = getInheritanceProperty();
if (inheritanceProperty != null) {
Cursor<? extends Statement> parentStatements = super.getStatements(subject, inheritanceProperty,
null, true);
try {
Statement parentStatement;
while ((parentStatement = parentStatements.next()) != null) {
Value value = parentStatement.getObject();
if (value instanceof Resource) {
getPropertyResourceValues(results, (Resource)value, predicate, false, visited, contexts);
}
}
}
finally {
parentStatements.close();
}
}
}
}
/**
* Retrieves the property defined in the ACL model as being the
* acl:InheritanceProperty. Note that we currently support only a single
* inheritance-property as multiple properties would mean that we need a
* multiple disjunctions in every query, severely affecting performance.
*
* @return
* @throws StoreException
*/
private synchronized URI getInheritanceProperty()
throws StoreException
{
// TODO: share this info across connections?
if (inheritanceProperty == null) {
Cursor<? extends Statement> statements = super.getStatements(null, RDF.TYPE,
ACL.INHERITANCE_PROPERTY, true);
try {
Statement st;
while ((st = statements.next()) != null) {
if (st.getSubject() instanceof URI) {
inheritanceProperty = (URI)st.getSubject();
break;
}
}
}
finally {
statements.close();
}
}
return inheritanceProperty;
}
/**
* Retrieve the permissions assigned to the supplied role and involving the
* supplied operation.
*
* @param roles
* a list of roles
* @param operation
* an operation identifier
* @return a Cursor containing URIs of permissions.
*/
private List<URI> getAssignedPermissions(List<URI> roles, URI operation)
throws StoreException
{
List<URI> permissions = new ArrayList<URI>();
for (URI role : roles) {
// TODO this would probably be more efficient using a SPARQL query.
Cursor<? extends Statement> statements = super.getStatements(null, ACL.TO_ROLE, role, true,
ACL.CONTEXT);
try {
Statement st;
while ((st = statements.next()) != null) {
Cursor<? extends Statement> permissionStatements = super.getStatements(st.getSubject(),
ACL.HAS_PERMISSION, null, true, ACL.CONTEXT);
try {
Statement permStat;
while ((permStat = permissionStatements.next()) != null) {
Cursor<? extends Statement> operationStatements = super.getStatements(
(URI)permStat.getObject(), ACL.HAS_OPERATION, operation, true, ACL.CONTEXT);
try {
if (operationStatements.next() != null) {
permissions.add((URI)permStat.getObject());
}
}
finally {
operationStatements.close();
}
}
}
finally {
permissionStatements.close();
}
}
}
finally {
statements.close();
}
}
return permissions;
}
private URI getAttributeValueForPermission(URI permission, URI attribute)
throws StoreException
{
Resource match = getPropertyResourceValue(permission, ACL.HAS_MATCH, false, ACL.CONTEXT);
URI attributeValue = (URI)getPropertyResourceValue(match, attribute, false, ACL.CONTEXT);
return attributeValue;
}
private List<URI> getRolesForUser(URI username)
throws StoreException
{
List<URI> roles = new ArrayList<URI>();
if (username != null) {
Cursor<? extends Statement> statements = super.getStatements(username, ACL.HAS_ROLE, null, true,
ACL.CONTEXT);
try {
Statement st;
while ((st = statements.next()) != null) {
Value value = st.getObject();
if (value instanceof URI) {
roles.add((URI)value);
}
}
}
finally {
statements.close();
}
}
return roles;
}
private URI getCurrentUser()
throws StoreException
{
Session session = SessionManager.get();
if (session != null) {
URI userId = session.getUserId();
if (userId != null) {
return userId;
}
String username = session.getUsername();
if (username != null) {
// backdoor for administrator user.
if (username.equals("administrator")) {
session.setUserId(ACL.ADMIN);
}
else {
Literal usernameLiteral = this.getValueFactory().createLiteral(username, XMLSchema.STRING);
Cursor<? extends Statement> statements = super.getStatements(null, ACL.USERNAME,
usernameLiteral, true, ACL.CONTEXT);
try {
Statement st;
if ((st = statements.next()) != null) {
session.setUserId((URI)st.getSubject());
}
}
finally {
statements.close();
}
}
return session.getUserId();
}
}
return null;
}
/***
* Retrieve all instances of acl:AccessAttribute from the ACL context.
*
* @return a list of URIs representing the attributes in terms of which
* object matches are defined. May be empty.
* @throws StoreException
*/
private synchronized List<URI> getAccessAttributes()
throws StoreException
{
// TODO: share this info across connections
if (accessAttributes == null) {
accessAttributes = new ArrayList<URI>();
Cursor<? extends Statement> statements = super.getStatements(null, RDF.TYPE, ACL.ACCESS_ATTRIBUTE,
false, ACL.CONTEXT);
try {
Statement st;
while ((st = statements.next()) != null) {
Resource subject = st.getSubject();
if (subject instanceof URI) {
accessAttributes.add((URI)subject);
}
}
}
finally {
statements.close();
}
}
return accessAttributes;
}
private boolean hasPermissionOnSubject(URI user, Resource subject, URI operation)
throws StoreException
{
boolean hasPermission = false;
// TODO this is a backdoor for testing purposes.
if (ACL.ADMIN.equals(user)) {
return true;
}
Collection<URI> attributes = getAccessAttributes();
HashMap<URI, Set<Resource>> attributeValues = new HashMap<URI, Set<Resource>>();
for (URI attribute : attributes) {
Set<Resource> attributeValueList = getPropertyResourceValues(subject, attribute, true);
if (!attributeValueList.isEmpty()) {
attributeValues.put(attribute, attributeValueList);
}
}
if (!attributeValues.isEmpty()) {
List<URI> permissions = getAssignedPermissions(getRolesForUser(user), operation);
for (URI permission : permissions) {
// check if all attributes match
boolean allMatch = true;
for (Entry<URI, Set<Resource>> entry : attributeValues.entrySet()) {
URI attribute = entry.getKey();
Set<Resource> attributeValueList = entry.getValue();
URI permissionAttributeValue = getAttributeValueForPermission(permission, attribute);
boolean isMatch = attributeValueList.contains(permissionAttributeValue);
if (!isMatch) {
allMatch = false;
break;
}
}
if (allMatch) {
hasPermission = true;
break;
}
}
}
else {
hasPermission = true;
}
return hasPermission;
}
protected class AccessControlQueryExpander extends QueryModelVisitorBase<StoreException> {
private final List<Var> handledSubjects = new ArrayList<Var>();
private List<URI> permissions;
@Override
public void meet(StatementPattern statementPattern)
throws StoreException
{
super.meet(statementPattern);
// keep a reference to the original parent.
QueryModelNode parent = statementPattern.getParentNode();
Var subjectVar = statementPattern.getSubjectVar();
if (handledSubjects.contains(subjectVar)) {
// we have already expanded the query for this particular subject
return;
}
handledSubjects.add(subjectVar);
/*
* Create this pattern:
*
* ?subject ?predicate ?object. (= the original statementPattern)
* OPTIONAL {
* { ?subject foo:accessAttr1 ?accessAttrValue1. }
* UNION
* { ?subject foo:inheritanceProp ?S1 .
* ?S1 foo:accessAttr1 ?accessAttrValue1.
* }
* { ?subject foo:accessAttr2 ?accessAttrValue2. }
* UNION
* { ?subject foo:inheritanceProp ?S2 .
* ?S2 foo:accessAttr1 ?accessAttrValue2.
* }
* ...
* }
*
* or in terms of the algebra:
*
* LeftJoin(
* SP(?subject, ?predicate, ?object),
* Join(
* Union(
* SP(?subject, accessAttr_1, ?accessAttrValue_1),
* Join (
* SP(?subject, inheritProp ?S_1),
* SP(?S_1, acccessAttr_1, ?accessAttrValue_1)
* )),
* Union(
* SP(?subject, accessAttr_2, ?accessAttrValue_2),
* Join (
* SP(?subject, inheritProp ?S_2),
* SP(?S_2, acccessAttr_2, ?accessAttrValue_2)
* )),
* ...
* )
* )
*/
List<URI> attributes = getAccessAttributes();
if (attributes == null || attributes.size() == 0) {
return;
}
// join of the attribute match expressions.
Join joinOfAttributePatterns = new Join();
URI inheritanceProp = getInheritanceProperty();
Var inheritPredVar = new Var("-acl_inherit_pred", inheritanceProp);
int i = 0;
List<Var> attributeVars = new ArrayList<Var>();
for (URI attribute : attributes) {
Var attributeVar = new Var("-acl_attr_" + i++);
attributeVars.add(attributeVar);
Var attributePredVar = new Var("-acl_attr_pred_" + i, attribute);
// SP(?subject, accessAttr_i, ?accessAttrValue_i)
StatementPattern attributePattern = new StatementPattern(subjectVar, attributePredVar,
attributeVar);
if (inheritanceProp != null) {
// create a union expression for this attribute.
Union union = new Union();
union.addArg(attributePattern);
// the join for checking if the access attribute is inherited.
Join inheritJoin = new Join();
Var inheritVar = new Var("-acl_inherited_value" + i);
// SP (?subject, inheritProp, ?S_i)
StatementPattern inheritPattern = new StatementPattern(subjectVar, inheritPredVar, inheritVar);
inheritJoin.addArg(inheritPattern);
// SP (?S_i, accessAttr_i, ?accessAttrValue_i)
StatementPattern inheritAttrPattern = new StatementPattern(inheritVar, attributePredVar,
attributeVar);
inheritJoin.addArg(inheritAttrPattern);
union.addArg(inheritJoin);
joinOfAttributePatterns.addArg(union);
}
else {
// no inheritance: the attribute can be matched with a simple
// statement pattern
joinOfAttributePatterns.addArg(attributePattern);
}
}
TupleExpr expandedPattern = null;
if (joinOfAttributePatterns.getNumberOfArguments() == 1) {
expandedPattern = new LeftJoin(statementPattern, joinOfAttributePatterns.getArg(0));
}
else {
expandedPattern = new LeftJoin(statementPattern, joinOfAttributePatterns);
}
// build an Or-ed set of filter conditions on the status and team.
Or filterConditions = new Or();
/* first condition is that none are bound: this is the case where the subject
* is not a restricted resource (and therefore has no associated attributes)
*
* And(Not(Bound(?acl_attr1)), Not(Bound(?acl_attr_1), ...)
*/
And and = new And();
for (Var attributeVar : attributeVars) {
and.addArg(new Not(new Bound(attributeVar)));
}
if (and.getArgs().size() == 1) {
filterConditions.addArg(and.getArg(0));
}
else {
filterConditions.addArg(and);
}
if (permissions == null) {
List<URI> roles = getRolesForUser(getCurrentUser());
permissions = getAssignedPermissions(roles, ACL.VIEW);
}
// for each permission, we add an additional condition to the filter,
// checking that either
// team, or status, or both match.
for (URI permission : permissions) {
And permissionCondition = new And();
for (int j = 0; j < attributes.size(); j++) {
URI attribute = attributes.get(j);
URI attributePermissionValue = getAttributeValueForPermission(permission, attribute);
Compare attributeValueCompare = null;
if (attributePermissionValue != null) {
attributeValueCompare = new Compare(attributeVars.get(j), new Var("acl_attr_val_" + j,
attributePermissionValue));
permissionCondition.addArg(attributeValueCompare);
}
}
if (permissionCondition.getNumberOfArguments() == 1) {
filterConditions.addArg(permissionCondition.getArg(0));
}
else {
// add the permission-defined condition to the set of Or-ed
// filter conditions.
filterConditions.addArg(permissionCondition);
}
}
// set the filter conditions on the query pattern
if (filterConditions.getNumberOfArguments() == 1) {
// no second argument in the or
expandedPattern = new Filter(expandedPattern, filterConditions.getArg(0));
}
else {
expandedPattern = new Filter(expandedPattern, filterConditions);
}
// expand the query.
parent.replaceChildNode(statementPattern, expandedPattern);
}
}
}