JdbcAclService: fixes PostgreSql issue

When setup the acl tables as specified in the Spring.io documentation
I have faced the following error on a PostgreSql database:
org.postgresql.util.PSQLException: ERROR: operator does not exist:
bigint = character varying.
This is because the acl_object_identity.object_id_identity column is
of type varchar(36) but it is not necessarily accessed with a value
of type String.

- JdbcAclService / JdbcMutableAclService: SQL query must match
  object_id_identity column specification
- JdbcAclService: changed JdbcTemplate to JdbcOperations for testability
- JdbcAclServiceTest: Increased test coverage,
  the integration tests using embedded db relates to this commit
cd8d2079ed

Fixes gh-5508
This commit is contained in:
Nena Raab 2018-11-06 10:31:58 +01:00 committed by Rob Winch
parent 1bfa38b1bd
commit d1a754fcf2
4 changed files with 188 additions and 35 deletions

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2004, 2005, 2006, 2017 Acegi Technology Pty Limited * Copyright 2004, 2005, 2006, 2017, 2018 Acegi Technology Pty Limited
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -27,6 +27,7 @@ import javax.sql.DataSource;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.ConversionService;
import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.core.RowMapper;
import org.springframework.security.acls.domain.ObjectIdentityImpl; import org.springframework.security.acls.domain.ObjectIdentityImpl;
@ -67,7 +68,7 @@ public class JdbcAclService implements AclService {
// ~ Instance fields // ~ Instance fields
// ================================================================================================ // ================================================================================================
protected final JdbcTemplate jdbcTemplate; protected final JdbcOperations jdbcOperations;
private final LookupStrategy lookupStrategy; private final LookupStrategy lookupStrategy;
private boolean aclClassIdSupported; private boolean aclClassIdSupported;
private String findChildrenSql = DEFAULT_SELECT_ACL_WITH_PARENT_SQL; private String findChildrenSql = DEFAULT_SELECT_ACL_WITH_PARENT_SQL;
@ -77,9 +78,13 @@ public class JdbcAclService implements AclService {
// =================================================================================================== // ===================================================================================================
public JdbcAclService(DataSource dataSource, LookupStrategy lookupStrategy) { public JdbcAclService(DataSource dataSource, LookupStrategy lookupStrategy) {
Assert.notNull(dataSource, "DataSource required"); this(new JdbcTemplate(dataSource), lookupStrategy);
}
public JdbcAclService(JdbcOperations jdbcOperations, LookupStrategy lookupStrategy) {
Assert.notNull(jdbcOperations, "JdbcOperations required");
Assert.notNull(lookupStrategy, "LookupStrategy required"); Assert.notNull(lookupStrategy, "LookupStrategy required");
this.jdbcTemplate = new JdbcTemplate(dataSource); this.jdbcOperations = jdbcOperations;
this.lookupStrategy = lookupStrategy; this.lookupStrategy = lookupStrategy;
this.aclClassIdUtils = new AclClassIdUtils(); this.aclClassIdUtils = new AclClassIdUtils();
} }
@ -88,15 +93,14 @@ public class JdbcAclService implements AclService {
// ======================================================================================================== // ========================================================================================================
public List<ObjectIdentity> findChildren(ObjectIdentity parentIdentity) { public List<ObjectIdentity> findChildren(ObjectIdentity parentIdentity) {
Object[] args = { parentIdentity.getIdentifier(), parentIdentity.getType() }; Object[] args = { parentIdentity.getIdentifier().toString(), parentIdentity.getType() };
List<ObjectIdentity> objects = jdbcTemplate.query(findChildrenSql, args, List<ObjectIdentity> objects = jdbcOperations.query(findChildrenSql, args,
new RowMapper<ObjectIdentity>() { new RowMapper<ObjectIdentity>() {
public ObjectIdentity mapRow(ResultSet rs, int rowNum) public ObjectIdentity mapRow(ResultSet rs, int rowNum)
throws SQLException { throws SQLException {
String javaType = rs.getString("class"); String javaType = rs.getString("class");
Serializable identifier = (Serializable) rs.getObject("obj_id"); Serializable identifier = (Serializable) rs.getObject("obj_id");
identifier = aclClassIdUtils.identifierFrom(identifier, rs); identifier = aclClassIdUtils.identifierFrom(identifier, rs);
return new ObjectIdentityImpl(javaType, identifier); return new ObjectIdentityImpl(javaType, identifier);
} }
}); });

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2004, 2005, 2006, 2017 Acegi Technology Pty Limited * Copyright 2004, 2005, 2006, 2017, 2018 Acegi Technology Pty Limited
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -135,7 +135,7 @@ public class JdbcMutableAclService extends JdbcAclService implements MutableAclS
if (acl.getEntries().isEmpty()) { if (acl.getEntries().isEmpty()) {
return; return;
} }
jdbcTemplate.batchUpdate(insertEntry, new BatchPreparedStatementSetter() { jdbcOperations.batchUpdate(insertEntry, new BatchPreparedStatementSetter() {
public int getBatchSize() { public int getBatchSize() {
return acl.getEntries().size(); return acl.getEntries().size();
} }
@ -170,7 +170,7 @@ public class JdbcMutableAclService extends JdbcAclService implements MutableAclS
protected void createObjectIdentity(ObjectIdentity object, Sid owner) { protected void createObjectIdentity(ObjectIdentity object, Sid owner) {
Long sidId = createOrRetrieveSidPrimaryKey(owner, true); Long sidId = createOrRetrieveSidPrimaryKey(owner, true);
Long classId = createOrRetrieveClassPrimaryKey(object.getType(), true, object.getIdentifier().getClass()); Long classId = createOrRetrieveClassPrimaryKey(object.getType(), true, object.getIdentifier().getClass());
jdbcTemplate.update(insertObjectIdentity, classId, object.getIdentifier(), sidId, jdbcOperations.update(insertObjectIdentity, classId, object.getIdentifier().toString(), sidId,
Boolean.TRUE); Boolean.TRUE);
} }
@ -184,7 +184,7 @@ public class JdbcMutableAclService extends JdbcAclService implements MutableAclS
* @return the primary key or null if not found * @return the primary key or null if not found
*/ */
protected Long createOrRetrieveClassPrimaryKey(String type, boolean allowCreate, Class idType) { protected Long createOrRetrieveClassPrimaryKey(String type, boolean allowCreate, Class idType) {
List<Long> classIds = jdbcTemplate.queryForList(selectClassPrimaryKey, List<Long> classIds = jdbcOperations.queryForList(selectClassPrimaryKey,
new Object[] { type }, Long.class); new Object[] { type }, Long.class);
if (!classIds.isEmpty()) { if (!classIds.isEmpty()) {
@ -193,13 +193,13 @@ public class JdbcMutableAclService extends JdbcAclService implements MutableAclS
if (allowCreate) { if (allowCreate) {
if (!isAclClassIdSupported()) { if (!isAclClassIdSupported()) {
jdbcTemplate.update(insertClass, type); jdbcOperations.update(insertClass, type);
} else { } else {
jdbcTemplate.update(insertClass, type, idType.getCanonicalName()); jdbcOperations.update(insertClass, type, idType.getCanonicalName());
} }
Assert.isTrue(TransactionSynchronizationManager.isSynchronizationActive(), Assert.isTrue(TransactionSynchronizationManager.isSynchronizationActive(),
"Transaction must be running"); "Transaction must be running");
return jdbcTemplate.queryForObject(classIdentityQuery, Long.class); return jdbcOperations.queryForObject(classIdentityQuery, Long.class);
} }
return null; return null;
@ -248,7 +248,7 @@ public class JdbcMutableAclService extends JdbcAclService implements MutableAclS
protected Long createOrRetrieveSidPrimaryKey(String sidName, boolean sidIsPrincipal, protected Long createOrRetrieveSidPrimaryKey(String sidName, boolean sidIsPrincipal,
boolean allowCreate) { boolean allowCreate) {
List<Long> sidIds = jdbcTemplate.queryForList(selectSidPrimaryKey, new Object[] { List<Long> sidIds = jdbcOperations.queryForList(selectSidPrimaryKey, new Object[] {
Boolean.valueOf(sidIsPrincipal), sidName }, Long.class); Boolean.valueOf(sidIsPrincipal), sidName }, Long.class);
if (!sidIds.isEmpty()) { if (!sidIds.isEmpty()) {
@ -256,10 +256,10 @@ public class JdbcMutableAclService extends JdbcAclService implements MutableAclS
} }
if (allowCreate) { if (allowCreate) {
jdbcTemplate.update(insertSid, Boolean.valueOf(sidIsPrincipal), sidName); jdbcOperations.update(insertSid, Boolean.valueOf(sidIsPrincipal), sidName);
Assert.isTrue(TransactionSynchronizationManager.isSynchronizationActive(), Assert.isTrue(TransactionSynchronizationManager.isSynchronizationActive(),
"Transaction must be running"); "Transaction must be running");
return jdbcTemplate.queryForObject(sidIdentityQuery, Long.class); return jdbcOperations.queryForObject(sidIdentityQuery, Long.class);
} }
return null; return null;
@ -311,7 +311,7 @@ public class JdbcMutableAclService extends JdbcAclService implements MutableAclS
* @param oidPrimaryKey the rows in acl_entry to delete * @param oidPrimaryKey the rows in acl_entry to delete
*/ */
protected void deleteEntries(Long oidPrimaryKey) { protected void deleteEntries(Long oidPrimaryKey) {
jdbcTemplate.update(deleteEntryByObjectIdentityForeignKey, oidPrimaryKey); jdbcOperations.update(deleteEntryByObjectIdentityForeignKey, oidPrimaryKey);
} }
/** /**
@ -325,7 +325,7 @@ public class JdbcMutableAclService extends JdbcAclService implements MutableAclS
*/ */
protected void deleteObjectIdentity(Long oidPrimaryKey) { protected void deleteObjectIdentity(Long oidPrimaryKey) {
// Delete the acl_object_identity row // Delete the acl_object_identity row
jdbcTemplate.update(deleteObjectIdentityByPrimaryKey, oidPrimaryKey); jdbcOperations.update(deleteObjectIdentityByPrimaryKey, oidPrimaryKey);
} }
/** /**
@ -339,8 +339,8 @@ public class JdbcMutableAclService extends JdbcAclService implements MutableAclS
*/ */
protected Long retrieveObjectIdentityPrimaryKey(ObjectIdentity oid) { protected Long retrieveObjectIdentityPrimaryKey(ObjectIdentity oid) {
try { try {
return jdbcTemplate.queryForObject(selectObjectIdentityPrimaryKey, Long.class, return jdbcOperations.queryForObject(selectObjectIdentityPrimaryKey, Long.class,
oid.getType(), oid.getIdentifier()); oid.getType(), oid.getIdentifier().toString());
} }
catch (DataAccessException notFound) { catch (DataAccessException notFound) {
return null; return null;
@ -409,7 +409,7 @@ public class JdbcMutableAclService extends JdbcAclService implements MutableAclS
Assert.notNull(acl.getOwner(), "Owner is required in this implementation"); Assert.notNull(acl.getOwner(), "Owner is required in this implementation");
Long ownerSid = createOrRetrieveSidPrimaryKey(acl.getOwner(), true); Long ownerSid = createOrRetrieveSidPrimaryKey(acl.getOwner(), true);
int count = jdbcTemplate.update(updateObjectIdentity, parentId, ownerSid, int count = jdbcOperations.update(updateObjectIdentity, parentId, ownerSid,
Boolean.valueOf(acl.isEntriesInheriting()), acl.getId()); Boolean.valueOf(acl.isEntriesInheriting()), acl.getId());
if (count != 1) { if (count != 1) {

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2016 the original author or authors. * Copyright 2002-2018 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -15,21 +15,16 @@
*/ */
package org.springframework.security.acls.jdbc; package org.springframework.security.acls.jdbc;
import static org.mockito.ArgumentMatchers.anyList; import org.junit.After;
import static org.mockito.Mockito.when;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.sql.DataSource;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner; import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.security.acls.domain.ObjectIdentityImpl; import org.springframework.security.acls.domain.ObjectIdentityImpl;
import org.springframework.security.acls.domain.PrincipalSid; import org.springframework.security.acls.domain.PrincipalSid;
import org.springframework.security.acls.model.Acl; import org.springframework.security.acls.model.Acl;
@ -37,19 +32,54 @@ import org.springframework.security.acls.model.NotFoundException;
import org.springframework.security.acls.model.ObjectIdentity; import org.springframework.security.acls.model.ObjectIdentity;
import org.springframework.security.acls.model.Sid; import org.springframework.security.acls.model.Sid;
import javax.sql.DataSource;
import java.util.*;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.AdditionalMatchers.aryEq;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.when;
/**
* Unit and Integration tests the ACL JdbcAclService using an
* in-memory database.
*
* @author Nena Raab
*/
@RunWith(MockitoJUnitRunner.class) @RunWith(MockitoJUnitRunner.class)
public class JdbcAclServiceTests { public class JdbcAclServiceTests {
private EmbeddedDatabase embeddedDatabase;
@Mock @Mock
private DataSource dataSource; private DataSource dataSource;
@Mock @Mock
private LookupStrategy lookupStrategy; private LookupStrategy lookupStrategy;
@Mock
JdbcOperations jdbcOperations;
private JdbcAclService aclServiceIntegration;
private JdbcAclService aclService; private JdbcAclService aclService;
@Before @Before
public void setUp() { public void setUp() {
aclService = new JdbcAclService(dataSource, lookupStrategy); aclService = new JdbcAclService(jdbcOperations, lookupStrategy);
aclServiceIntegration = new JdbcAclService(embeddedDatabase, lookupStrategy);
}
@Before
public void setUpEmbeddedDatabase() {
embeddedDatabase = new EmbeddedDatabaseBuilder()//
.addScript("createAclSchemaWithAclClassIdType.sql")
.addScript("db/sql/test_data_hierarchy.sql")
.build();
}
@After
public void tearDownEmbeddedDatabase() {
embeddedDatabase.shutdown();
} }
// SEC-1898 // SEC-1898
@ -64,4 +94,106 @@ public class JdbcAclServiceTests {
aclService.readAclById(objectIdentity, sids); aclService.readAclById(objectIdentity, sids);
} }
@Test
public void findOneChildren() {
List<ObjectIdentity> result = new ArrayList<>();
result.add(new ObjectIdentityImpl(Object.class, "5577"));
Object[] args = {"1", "org.springframework.security.acls.jdbc.JdbcAclServiceTests$MockLongIdDomainObject"};
when(
jdbcOperations.query(anyString(),
aryEq(args), any(RowMapper.class))).thenReturn(result);
ObjectIdentity objectIdentity = new ObjectIdentityImpl(MockLongIdDomainObject.class, 1L);
List<ObjectIdentity> objectIdentities = aclService.findChildren(objectIdentity);
assertThat(objectIdentities.size()).isEqualTo(1);
assertThat(objectIdentities.get(0).getIdentifier()).isEqualTo("5577");
}
@Test
public void findNoChildren() {
ObjectIdentity objectIdentity = new ObjectIdentityImpl(MockLongIdDomainObject.class, 1L);
List<ObjectIdentity> objectIdentities = aclService.findChildren(objectIdentity);
assertThat(objectIdentities).isNull();
}
// ~ Some integration tests
// ========================================================================================================
@Test
public void findChildrenWithoutIdType() {
ObjectIdentity objectIdentity = new ObjectIdentityImpl(MockLongIdDomainObject.class, 4711L);
List<ObjectIdentity> objectIdentities = aclServiceIntegration.findChildren(objectIdentity);
assertThat(objectIdentities.size()).isEqualTo(1);
assertThat(objectIdentities.get(0).getType()).isEqualTo(MockUntypedIdDomainObject.class.getName());
assertThat(objectIdentities.get(0).getIdentifier()).isEqualTo(5000L);
}
@Test
public void findChildrenForUnknownObject() {
ObjectIdentity objectIdentity = new ObjectIdentityImpl(Object.class, 33);
List<ObjectIdentity> objectIdentities = aclServiceIntegration.findChildren(objectIdentity);
assertThat(objectIdentities).isNull();
}
@Test
public void findChildrenOfIdTypeLong() {
ObjectIdentity objectIdentity = new ObjectIdentityImpl("location", "US-PAL");
List<ObjectIdentity> objectIdentities = aclServiceIntegration.findChildren(objectIdentity);
assertThat(objectIdentities.size()).isEqualTo(2);
assertThat(objectIdentities.get(0).getType()).isEqualTo(MockLongIdDomainObject.class.getName());
assertThat(objectIdentities.get(0).getIdentifier()).isEqualTo(4711L);
assertThat(objectIdentities.get(1).getType()).isEqualTo(MockLongIdDomainObject.class.getName());
assertThat(objectIdentities.get(1).getIdentifier()).isEqualTo(4712L);
}
@Test
public void findChildrenOfIdTypeString() {
ObjectIdentity objectIdentity = new ObjectIdentityImpl("location", "US");
aclServiceIntegration.setAclClassIdSupported(true);
List<ObjectIdentity> objectIdentities = aclServiceIntegration.findChildren(objectIdentity);
assertThat(objectIdentities.size()).isEqualTo(1);
assertThat(objectIdentities.get(0).getType()).isEqualTo("location");
assertThat(objectIdentities.get(0).getIdentifier()).isEqualTo("US-PAL");
}
@Test
public void findChildrenOfIdTypeUUID() {
ObjectIdentity objectIdentity = new ObjectIdentityImpl(MockUntypedIdDomainObject.class, 5000L);
aclServiceIntegration.setAclClassIdSupported(true);
List<ObjectIdentity> objectIdentities = aclServiceIntegration.findChildren(objectIdentity);
assertThat(objectIdentities.size()).isEqualTo(1);
assertThat(objectIdentities.get(0).getType()).isEqualTo("costcenter");
assertThat(objectIdentities.get(0).getIdentifier()).isEqualTo(UUID.fromString("25d93b3f-c3aa-4814-9d5e-c7c96ced7762"));
}
private class MockLongIdDomainObject {
private Object id;
public Object getId() {
return id;
}
public void setId(Object id) {
this.id = id;
}
}
private class MockUntypedIdDomainObject {
private Object id;
public Object getId() {
return id;
}
public void setId(Object id) {
this.id = id;
}
}
} }

View File

@ -0,0 +1,17 @@
--- insert ACL data
INSERT INTO ACL_SID (ID, PRINCIPAL, SID) VALUES
(10, true, 'user');
INSERT INTO acl_class (id, class, class_id_type) VALUES
(20,'location','java.lang.String'),
(21,'org.springframework.security.acls.jdbc.JdbcAclServiceTests$MockLongIdDomainObject','java.lang.Long'),
(22,'org.springframework.security.acls.jdbc.JdbcAclServiceTests$MockUntypedIdDomainObject',''),
(23,'costcenter','java.util.UUID');
INSERT INTO acl_object_identity (id, object_id_class, object_id_identity, parent_object, owner_sid, entries_inheriting) VALUES
(1,20,'US',NULL,10,false),
(2,20,'US-PAL',1,10,true),
(3,21,'4711',2,10,true),
(4,21,'4712',2,10,true),
(5,22,'5000',3,10,true),
(6,23,'25d93b3f-c3aa-4814-9d5e-c7c96ced7762',5,10,true);