/* Copyright 2013 Rigas Grigoropoulos * * Licensed 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 eu.europeana.aas.acl.repository; import com.datastax.driver.core.ConsistencyLevel; import com.datastax.driver.core.ResultSet; import com.datastax.driver.core.Row; import com.datastax.driver.core.Session; import com.datastax.driver.core.exceptions.AlreadyExistsException; import com.datastax.driver.core.querybuilder.Batch; import com.datastax.driver.core.querybuilder.QueryBuilder; import eu.europeana.aas.acl.model.AclEntry; import eu.europeana.aas.acl.model.AclObjectIdentity; import eu.europeana.aas.acl.repository.exceptions.AclAlreadyExistsException; import eu.europeana.aas.acl.repository.exceptions.AclNotFoundException; import eu.europeana.cloud.cassandra.CassandraConnectionProvider; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.util.Assert; import java.util.ArrayList; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.TreeSet; /** * Implementation of <code>AclRepository</code> using the DataStax Java Driver. * * @author Rigas Grigoropoulos * @author Markus.Muhr@theeuropeanlibrary.org */ public final class CassandraAclRepository implements AclRepository { private static final Log LOG = LogFactory .getLog(CassandraAclRepository.class); private static final String AOI_TABLE = "aois"; private static final String CHILDREN_TABLE = "children"; private static final String ACL_TABLE = "acls"; private static final String[] AOI_KEYS = new String[] { "id", "objId", "objClass", "isInheriting", "owner", "isOwnerPrincipal", "parentObjId", "parentObjClass" }; private static final String[] CHILD_KEYS = new String[] { "id", "childId", "objId", "objClass" }; private static final String[] ACL_KEYS = new String[] { "id", "aclOrder", "sid", "mask", "isSidPrincipal", "isGranting", "isAuditSuccess", "isAuditFailure" }; // private String replicationStrategy = "SimpleStrategy"; // private int replicationFactor = 3; private final Session session; private final String keyspace; /** * Constructs a new <code>CassandraAclRepositoryImpl</code>. * * @param provider * providing the <code>Session</code> to use for connectivity * with Cassandra. * @param initSchema * whether the keyspace and schema for storing ACLs should // * * be created. */ public CassandraAclRepository(CassandraConnectionProvider provider, boolean initSchema) { this(provider.getSession(), provider.getKeyspaceName(), initSchema); } /** * Constructs a new <code>CassandraAclRepositoryImpl</code>. * * @param session * the <code>Session</code> to use for connectivity with * Cassandra. * @param keyspace * whether the keyspace and schema for storing ACLs should be * created. */ public CassandraAclRepository(Session session, String keyspace) { this.session = session; this.keyspace = keyspace; } // /** // * Constructs a new <code>CassandraAclRepositoryImpl</code> and optionally // * creates the Cassandra keyspace and schema for storing ACLs. // * // * @param session the <code>Session</code> to use for connectivity with // * Cassandra. // * @param initSchema whether the keyspace and schema for storing ACLs // should // * be created. // * @param replicationStrategy the replication strategy to use when // creating // * the keyspace. // * @param replicationFactor the replication factor to use when creating // the // * keyspace. // */ // public CassandraAclRepository(Session session, String keyspace, boolean // initSchema, String replicationStrategy, int replicationFactor) { // this(session, keyspace); // if (initSchema) { // // this.replicationFactor = replicationFactor; // // this.replicationStrategy = replicationStrategy; // // // createkeyspace(); // createAoisTable(); // createChilrenTable(); // createAclsTable(); // } // } /** * Constructs a new <code>CassandraAclRepositoryImpl</code> and optionally * creates the Cassandra keyspace and schema for storing ACLs. * * @param session * the <code>Session</code> to use for connectivity with * Cassandra. * @param keyspace * whether the keyspace and schema for storing ACLs should be * created. * @param initSchema * whether the keyspace and schema for storing ACLs should be * created. */ public CassandraAclRepository(Session session, String keyspace, boolean initSchema) { this(session, keyspace); if (initSchema) { // createkeyspace(); createAoisTable(); createChilrenTable(); createAclsTable(); } } @Override public Map<AclObjectIdentity, Set<AclEntry>> findAcls(List<AclObjectIdentity> objectIdsToLookup) { assertAclObjectIdentityList(objectIdsToLookup); if (LOG.isDebugEnabled()) { LOG.debug("BEGIN findAcls: objectIdentities: " + objectIdsToLookup); } Map<AclObjectIdentity, Set<AclEntry>> resultMap = new HashMap<>(); List<String> ids = new ArrayList<>(); for (AclObjectIdentity entry : objectIdsToLookup) { ids.add(entry.getRowId()); } ResultSet resultSet = session.execute(QueryBuilder.select().all().from(keyspace, AOI_TABLE) .where(QueryBuilder.in("id", ids.toArray())).setConsistencyLevel(ConsistencyLevel.QUORUM)); for (Row row : resultSet.all()) { resultMap.put(convertToAclObjectIdentity(row, true), new TreeSet<>(new Comparator<AclEntry>() { @Override public int compare(AclEntry o1, AclEntry o2) { return new Integer(o1.getOrder()).compareTo(o2.getOrder()); } })); } resultSet = session.execute(QueryBuilder.select().all().from(keyspace, ACL_TABLE) .where(QueryBuilder.in("id", ids.toArray())).setConsistencyLevel(ConsistencyLevel.QUORUM)); for (Row row : resultSet.all()) { String aoiId = row.getString("id"); AclEntry aclEntry = new AclEntry(); aclEntry.setAuditFailure(row.getBool("isAuditFailure")); aclEntry.setAuditSuccess(row.getBool("isAuditSuccess")); aclEntry.setGranting(row.getBool("isGranting")); aclEntry.setMask(row.getInt("mask")); aclEntry.setOrder(row.getInt("aclOrder")); aclEntry.setSid(row.getString("sid")); aclEntry.setSidPrincipal(row.getBool("isSidPrincipal")); aclEntry.setId(aoiId + ":" + aclEntry.getSid() + ":" + aclEntry.getOrder()); for (Entry<AclObjectIdentity, Set<AclEntry>> entry : resultMap.entrySet()) { if (entry.getKey().getRowId().equals(aoiId)) { entry.getValue().add(aclEntry); break; } } } if (LOG.isDebugEnabled()) { LOG.debug("END findAcls: objectIdentities: " + resultMap.keySet() + ", aclEntries: " + resultMap.values()); } return resultMap; } @Override public AclObjectIdentity findAclObjectIdentity(AclObjectIdentity objectId) { assertAclObjectIdentity(objectId); if (LOG.isDebugEnabled()) { LOG.debug("BEGIN findAclObjectIdentity: objectIdentity: " + objectId); } Row row = session.execute( QueryBuilder.select().all().from(keyspace, AOI_TABLE) .where(QueryBuilder.eq("id", objectId.getRowId())).setConsistencyLevel(ConsistencyLevel.QUORUM)) .one(); AclObjectIdentity objectIdentity = convertToAclObjectIdentity(row, true); if (LOG.isDebugEnabled()) { LOG.debug("END findAclObjectIdentity: objectIdentity: " + objectIdentity); } return objectIdentity; } @Override public List<AclObjectIdentity> findAclObjectIdentityChildren(AclObjectIdentity objectId) { assertAclObjectIdentity(objectId); if (LOG.isDebugEnabled()) { LOG.debug("BEGIN findAclObjectIdentityChildren: objectIdentity: " + objectId); } List<AclObjectIdentity> result = new ArrayList<>(); ResultSet resultSet = session.execute(QueryBuilder.select().all().from(keyspace, CHILDREN_TABLE) .where(QueryBuilder.eq("id", objectId.getRowId())).setConsistencyLevel(ConsistencyLevel.QUORUM)); for (Row row : resultSet.all()) { result.add(convertToAclObjectIdentity(row, false)); } if (LOG.isDebugEnabled()) { LOG.debug("END findAclObjectIdentityChildren: children: " + result); } return result; } @Override public void deleteAcls(List<AclObjectIdentity> objectIdsToDelete) { assertAclObjectIdentityList(objectIdsToDelete); if (LOG.isDebugEnabled()) { LOG.debug("BEGIN deleteAcls: objectIdsToDelete: " + objectIdsToDelete); } List<String> ids = new ArrayList<>(); for (AclObjectIdentity entry : objectIdsToDelete) { ids.add(entry.getRowId()); } Batch batch = QueryBuilder.batch(); batch.add(QueryBuilder.delete().all().from(keyspace, AOI_TABLE).where(QueryBuilder.in("id", ids.toArray()))); batch.add(QueryBuilder.delete().all().from(keyspace, CHILDREN_TABLE) .where(QueryBuilder.in("id", ids.toArray()))).setConsistencyLevel(ConsistencyLevel.QUORUM); session.execute(batch); if (LOG.isDebugEnabled()) { LOG.debug("END deleteAcls"); } } @Override public void saveAcl(AclObjectIdentity aoi) throws AclAlreadyExistsException { assertAclObjectIdentity(aoi); if (LOG.isDebugEnabled()) { LOG.debug("BEGIN saveAcl: aclObjectIdentity: " + aoi); } // Check this object identity hasn't already been persisted if (findAclObjectIdentity(aoi) != null) { throw new AclAlreadyExistsException("Object identity '" + aoi + "' already exists"); } Batch batch = QueryBuilder.batch(); batch.add(QueryBuilder.insertInto(keyspace, AOI_TABLE).values( AOI_KEYS, new Object[] { aoi.getRowId(), aoi.getId(), aoi.getObjectClass(), aoi.isEntriesInheriting(), aoi.getOwnerId(), aoi.isOwnerPrincipal(), aoi.getParentObjectId(), aoi.getParentObjectClass() })).setConsistencyLevel(ConsistencyLevel.QUORUM); if (aoi.getParentRowId() != null) { batch.add(QueryBuilder.insertInto(keyspace, CHILDREN_TABLE).values( CHILD_KEYS, new Object[] { aoi.getParentRowId(), aoi.getRowId(), aoi.getId(), aoi.getObjectClass() })).setConsistencyLevel(ConsistencyLevel.QUORUM); } session.execute(batch); if (LOG.isDebugEnabled()) { LOG.debug("END saveAcl"); } } @Override public void updateAcl(AclObjectIdentity aoi, List<AclEntry> entries) throws AclNotFoundException { assertAclObjectIdentity(aoi); if (LOG.isDebugEnabled()) { LOG.debug("BEGIN updateAcl: aclObjectIdentity: " + aoi + ", entries: " + entries); } // Check this object identity is already persisted AclObjectIdentity persistedAoi = findAclObjectIdentity(aoi); if (persistedAoi == null) { throw new AclNotFoundException("Object identity '" + aoi + "' does not exist"); } // Update AOI & delete existing ACLs Batch batch = QueryBuilder.batch(); batch.add(QueryBuilder.insertInto(keyspace, AOI_TABLE).values( AOI_KEYS, new Object[] { aoi.getRowId(), aoi.getId(), aoi.getObjectClass(), aoi.isEntriesInheriting(), aoi.getOwnerId(), aoi.isOwnerPrincipal(), aoi.getParentObjectId(), aoi.getParentObjectClass() })); batch.add(QueryBuilder.delete().all().from(keyspace, ACL_TABLE) .where(QueryBuilder.eq("id", aoi.getRowId()))); // Check if parent is different and delete from children table boolean parentChanged = false; if (!(persistedAoi.getParentRowId() == null ? aoi.getParentRowId() == null : persistedAoi.getParentRowId().equals(aoi.getParentRowId()))) { parentChanged = true; if (persistedAoi.getParentRowId() != null) { batch.add(QueryBuilder .delete() .all() .from(keyspace, CHILDREN_TABLE) .where(QueryBuilder.eq("id", persistedAoi.getParentRowId())) .and(QueryBuilder.eq("childId", aoi.getRowId()))).setConsistencyLevel(ConsistencyLevel.QUORUM); } } session.execute(batch); // Update ACLs & children table batch = QueryBuilder.batch(); boolean executeBatch = false; if (entries != null && !entries.isEmpty()) { for (AclEntry entry : entries) { batch.add(QueryBuilder.insertInto(keyspace, ACL_TABLE) .values(ACL_KEYS, new Object[] { aoi.getRowId(), entry.getOrder(), entry.getSid(), entry.getMask(), entry.isSidPrincipal(), entry.isGranting(), entry.isAuditSuccess(), entry.isAuditFailure() })); } executeBatch = true; } if (parentChanged) { if (aoi.getParentRowId() != null) { batch.add(QueryBuilder.insertInto(keyspace, CHILDREN_TABLE) .values(CHILD_KEYS, new Object[] { aoi.getParentRowId(), aoi.getRowId(), aoi.getId(), aoi.getObjectClass() })).setConsistencyLevel(ConsistencyLevel.QUORUM); } executeBatch = true; } if (executeBatch) { session.execute(batch); } if (LOG.isDebugEnabled()) { LOG.debug("END updateAcl"); } } /** * Validates all <code>AclObjectIdentity</code> objects in the list. * * @param aoiList * a list of <code>AclObjectIdentity</code> objects to validate. */ private void assertAclObjectIdentityList(List<AclObjectIdentity> aoiList) { Assert.notEmpty(aoiList, "The AclObjectIdentity list cannot be empty"); for (AclObjectIdentity aoi : aoiList) { assertAclObjectIdentity(aoi); } } /** * Validates an <code>AclObjectIdentity</code> object. * * @param aoi * the <code>AclObjectIdentity</code> object to validate. */ private void assertAclObjectIdentity(AclObjectIdentity aoi) { Assert.notNull(aoi, "The AclObjectIdentity cannot be null"); Assert.notNull(aoi.getId(), "The AclObjectIdentity id cannot be null"); Assert.notNull(aoi.getObjectClass(), "The AclObjectIdentity objectClass cannot be null"); } /** * Converts a <code>Row</code> from a Cassandra result to an * <code>AclObjectIdentity</code> object. * * @param row * the <code>Row</code> representing an * <code>AclObjectIdentity</code>. * @param fullObject * whether the returned <code>AclObjectIdentity</code> object * will contain only identification parameters or will be fully * populated. * @return an <code>AclObjectIdentity</code> object with the values * retrieved from Cassandra. */ private AclObjectIdentity convertToAclObjectIdentity(Row row, boolean fullObject) { AclObjectIdentity result = null; if (row != null) { result = new AclObjectIdentity(); result.setId(row.getString("objId")); result.setObjectClass(row.getString("objClass")); if (fullObject) { result.setOwnerId(row.getString("owner")); result.setEntriesInheriting(row.getBool("isInheriting")); result.setOwnerPrincipal(row.getBool("isOwnerPrincipal")); result.setParentObjectClass(row.getString("parentObjClass")); result.setParentObjectId(row.getString("parentObjId")); } } return result; } /** * Creates the schema for the table holding <code>AclObjectIdentity</code> * representations. */ public void createAoisTable() { try { // session.execute("CREATE TABLE " + keyspace + ".aois (" session.execute("CREATE TABLE aois (" + "id varchar PRIMARY KEY," + "objId varchar," + "objClass varchar," + "isInheriting boolean," + "owner varchar," + "isOwnerPrincipal boolean," + "parentObjId varchar," + "parentObjClass varchar" + ");"); } catch (AlreadyExistsException e) { LOG.warn(e); } } /** * Creates the schema for the table holding <code>AclObjectIdentity</code> * children. */ public void createChilrenTable() { try { // session.execute("CREATE TABLE " + keyspace + ".children (" session.execute("CREATE TABLE children (" + "id varchar," + "childId varchar," + "objId varchar," + "objClass varchar," + "PRIMARY KEY (id, childId)" + ");"); } catch (AlreadyExistsException e) { LOG.warn(e); } } /** * Creates the schema for the table holding <code>AclEntry</code> * representations. */ public void createAclsTable() { try { // session.execute("CREATE TABLE " + keyspace + ".acls (" session.execute("CREATE TABLE acls (" + "id varchar," + "sid varchar," + "aclOrder int," + "mask int," + "isSidPrincipal boolean," + "isGranting boolean," + "isAuditSuccess boolean," + "isAuditFailure boolean," + "PRIMARY KEY (id, sid, aclOrder)" + ");"); } catch (AlreadyExistsException e) { LOG.warn(e); } } // /** // * Creates the schema for the 'SpringSecurityAclCassandra' keyspace. // */ // public void createkeyspace() { // try { // session.execute("CREATE keyspace " + keyspace // + " WITH replication " + "= {'class':'" + replicationStrategy + // "', 'replication_factor':" + replicationFactor + "};"); // } catch (AlreadyExistsException e) { // LOG.warn(e); // } // } // public Session getSession() { // return session; // } }