PL-6868 Do not check security in DataManager on the middleware by default

This commit is contained in:
Konstantin Krivopustov 2016-03-11 19:04:14 +04:00
parent 18f69d29c9
commit 36a43fcf10
9 changed files with 293 additions and 50 deletions

View File

@ -122,4 +122,9 @@ public class DataManagerClientImpl implements DataManager {
Collections.singleton(entity));
commit(context);
}
@Override
public DataManager secure() {
return this;
}
}

View File

@ -16,6 +16,7 @@ import com.haulmont.cuba.core.entity.BaseGenericIdEntity;
import com.haulmont.cuba.core.entity.CategoryAttributeValue;
import com.haulmont.cuba.core.entity.Entity;
import com.haulmont.cuba.core.global.*;
import com.haulmont.cuba.core.sys.AppContext;
import com.haulmont.cuba.security.entity.ConstraintOperationType;
import com.haulmont.cuba.security.entity.EntityOp;
import com.haulmont.cuba.security.entity.PermissionType;
@ -25,6 +26,9 @@ import org.springframework.stereotype.Component;
import javax.annotation.Nullable;
import javax.inject.Inject;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.*;
import static org.apache.commons.lang.StringUtils.isBlank;
@ -45,7 +49,7 @@ public class DataManagerBean implements DataManager {
protected ViewRepository viewRepository;
@Inject
protected Configuration configuration;
protected ServerConfig serverConfig;
@Inject
protected PersistenceSecurity security;
@ -76,7 +80,7 @@ public class DataManagerBean implements DataManager {
final MetaClass metaClass = metadata.getSession().getClassNN(context.getMetaClass());
if (!security.isEntityOpPermitted(metaClass, EntityOp.READ)) {
if (!isEntityOpPermitted(metaClass, EntityOp.READ)) {
log.debug("reading of " + metaClass + " not permitted, returning null");
return null;
}
@ -128,7 +132,7 @@ public class DataManagerBean implements DataManager {
MetaClass metaClass = metadata.getClassNN(context.getMetaClass());
if (!security.isEntityOpPermitted(metaClass, EntityOp.READ)) {
if (!isEntityOpPermitted(metaClass, EntityOp.READ)) {
log.debug("reading of " + metaClass + " not permitted, returning empty list");
return Collections.emptyList();
}
@ -143,7 +147,7 @@ public class DataManagerBean implements DataManager {
persistence.getEntityManagerContext().setDbHints(context.getDbHints());
boolean ensureDistinct = false;
if (configuration.getConfig(ServerConfig.class).getInMemoryDistinct() && context.getQuery() != null) {
if (serverConfig.getInMemoryDistinct() && context.getQuery() != null) {
QueryTransformer transformer = QueryTransformerFactory.createTransformer(
context.getQuery().getQueryString());
ensureDistinct = transformer.removeDistinct();
@ -186,7 +190,7 @@ public class DataManagerBean implements DataManager {
MetaClass metaClass = metadata.getClassNN(context.getMetaClass());
if (!security.isEntityOpPermitted(metaClass, EntityOp.READ)) {
if (!isEntityOpPermitted(metaClass, EntityOp.READ)) {
log.debug("reading of " + metaClass + " not permitted, returning 0");
return 0;
}
@ -344,7 +348,7 @@ public class DataManagerBean implements DataManager {
tx.end();
}
if (userSessionSource.getUserSession().hasConstraints()) {
if (isAuthorizationRequired() && userSessionSource.getUserSession().hasConstraints()) {
security.applyConstraints(res);
}
@ -364,7 +368,8 @@ public class DataManagerBean implements DataManager {
}
protected void checkOperationPermitted(Entity entity, ConstraintOperationType operationType) {
if (userSessionSource.getUserSession().hasConstraints()
if (isAuthorizationRequired()
&& userSessionSource.getUserSession().hasConstraints()
&& security.hasConstraints(entity.getMetaClass())
&& !security.isPermitted(entity, operationType)) {
throw new RowLevelSecurityException(
@ -463,6 +468,19 @@ public class DataManagerBean implements DataManager {
commit(context);
}
@Override
public DataManager secure() {
if (serverConfig.getDataManagerChecksSecurityOnMiddleware()) {
return this;
} else {
return (DataManager) Proxy.newProxyInstance(
getClass().getClassLoader(),
new Class[]{DataManager.class},
new SecureDataManagerInvocationHandler(this)
);
}
}
protected Query createQuery(EntityManager em, LoadContext context) {
LoadContext.Query contextQuery = context.getQuery();
if ((contextQuery == null || isBlank(contextQuery.getQueryString()))
@ -621,10 +639,19 @@ public class DataManagerBean implements DataManager {
}
protected void checkPermission(MetaClass metaClass, EntityOp operation) {
if (!security.isEntityOpPermitted(metaClass, operation))
if (!isEntityOpPermitted(metaClass, operation))
throw new AccessDeniedException(PermissionType.ENTITY_OP, metaClass.getName());
}
private boolean isEntityOpPermitted(MetaClass metaClass, EntityOp operation) {
return !isAuthorizationRequired() || security.isEntityOpPermitted(metaClass, operation);
}
protected boolean isAuthorizationRequired() {
return serverConfig.getDataManagerChecksSecurityOnMiddleware()
|| AppContext.getSecurityContext().isAuthorizationRequired();
}
/**
* Update references from newly persisted entities to merged detached entities. Otherwise a new entity can
* contain a stale instance of merged entity.
@ -688,7 +715,7 @@ public class DataManagerBean implements DataManager {
}
protected boolean needToApplyConstraints(LoadContext context) {
if (!userSessionSource.getUserSession().hasConstraints()) {
if (!isAuthorizationRequired() || !userSessionSource.getUserSession().hasConstraints()) {
return false;
}
@ -721,4 +748,24 @@ public class DataManagerBean implements DataManager {
}
return classes;
}
private class SecureDataManagerInvocationHandler implements InvocationHandler {
private final DataManager impl;
private SecureDataManagerInvocationHandler(DataManager impl) {
this.impl = impl;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
boolean authorizationRequired = AppContext.getSecurityContext().isAuthorizationRequired();
AppContext.getSecurityContext().setAuthorizationRequired(true);
try {
return method.invoke(impl, args);
} finally {
AppContext.getSecurityContext().setAuthorizationRequired(authorizationRequired);
}
}
}
}

View File

@ -27,22 +27,22 @@ public class DataServiceBean implements DataService {
@Override
public Set<Entity> commit(CommitContext context) {
return dataManager.commit(context);
return dataManager.secure().commit(context);
}
@Override
@Nullable
public <E extends Entity> E load(LoadContext<E> context) {
return dataManager.load(context);
return dataManager.secure().load(context);
}
@Override
public <E extends Entity> List<E> loadList(LoadContext<E> context) {
return dataManager.loadList(context);
return dataManager.secure().loadList(context);
}
@Override
public long getCount(LoadContext<? extends Entity> context) {
return dataManager.getCount(context);
return dataManager.secure().getCount(context);
}
}

View File

@ -148,4 +148,12 @@ public interface ServerConfig extends Config {
@Property("cuba.keyForSecurityTokenEncryption")
@DefaultString("CUBA.Platform")
String getKeyForSecurityTokenEncryption();
/**
* Indicates that {@code DataManager} should always apply security restrictions on the middleware.
*/
@Property("cuba.dataManagerChecksSecurityOnMiddleware")
@Source(type = SourceType.DATABASE)
@DefaultBoolean(false)
boolean getDataManagerChecksSecurityOnMiddleware();
}

View File

@ -9,11 +9,12 @@ import com.haulmont.cuba.core.entity.BaseEntity;
import com.haulmont.cuba.core.entity.annotation.Listeners;
import com.haulmont.cuba.core.global.AppBeans;
import com.haulmont.cuba.core.listener.*;
import com.haulmont.cuba.core.sys.AppContext;
import org.apache.commons.lang.ClassUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import javax.inject.Inject;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
@ -166,42 +167,54 @@ public class EntityListenerManager {
return;
List listeners = getListener(entity.getClass(), type);
for (Object listener : listeners) {
switch (type) {
case BEFORE_DETACH:
logExecution(type, entity);
((BeforeDetachEntityListener) listener).onBeforeDetach(entity, persistence.getEntityManager());
break;
case BEFORE_ATTACH:
logExecution(type, entity);
((BeforeAttachEntityListener) listener).onBeforeAttach(entity);
break;
case BEFORE_INSERT:
logExecution(type, entity);
((BeforeInsertEntityListener) listener).onBeforeInsert(entity);
break;
case AFTER_INSERT:
logExecution(type, entity);
((AfterInsertEntityListener) listener).onAfterInsert(entity);
break;
case BEFORE_UPDATE:
logExecution(type, entity);
((BeforeUpdateEntityListener) listener).onBeforeUpdate(entity);
break;
case AFTER_UPDATE:
logExecution(type, entity);
((AfterUpdateEntityListener) listener).onAfterUpdate(entity);
break;
case BEFORE_DELETE:
logExecution(type, entity);
((BeforeDeleteEntityListener) listener).onBeforeDelete(entity);
break;
case AFTER_DELETE:
logExecution(type, entity);
((AfterDeleteEntityListener) listener).onAfterDelete(entity);
break;
default:
throw new UnsupportedOperationException("Unsupported EntityListenerType: " + type);
boolean saved = false;
if (AppContext.getSecurityContext() != null) { // can be null before login when detaching entities
saved = AppContext.getSecurityContext().isAuthorizationRequired();
AppContext.getSecurityContext().setAuthorizationRequired(false);
}
try {
for (Object listener : listeners) {
switch (type) {
case BEFORE_DETACH:
logExecution(type, entity);
((BeforeDetachEntityListener) listener).onBeforeDetach(entity, persistence.getEntityManager());
break;
case BEFORE_ATTACH:
logExecution(type, entity);
((BeforeAttachEntityListener) listener).onBeforeAttach(entity);
break;
case BEFORE_INSERT:
logExecution(type, entity);
((BeforeInsertEntityListener) listener).onBeforeInsert(entity);
break;
case AFTER_INSERT:
logExecution(type, entity);
((AfterInsertEntityListener) listener).onAfterInsert(entity);
break;
case BEFORE_UPDATE:
logExecution(type, entity);
((BeforeUpdateEntityListener) listener).onBeforeUpdate(entity);
break;
case AFTER_UPDATE:
logExecution(type, entity);
((AfterUpdateEntityListener) listener).onAfterUpdate(entity);
break;
case BEFORE_DELETE:
logExecution(type, entity);
((BeforeDeleteEntityListener) listener).onBeforeDelete(entity);
break;
case AFTER_DELETE:
logExecution(type, entity);
((AfterDeleteEntityListener) listener).onAfterDelete(entity);
break;
default:
throw new UnsupportedOperationException("Unsupported EntityListenerType: " + type);
}
}
} finally {
if (AppContext.getSecurityContext() != null) {
AppContext.getSecurityContext().setAuthorizationRequired(saved);
}
}
}

View File

@ -0,0 +1,123 @@
/*
* Copyright (c) 2008-2016 Haulmont. All rights reserved.
* Use is subject to license terms, see http://www.cuba-platform.com/license for details.
*/
package com.haulmont.cuba.security;
import com.haulmont.cuba.core.EntityManager;
import com.haulmont.cuba.core.Transaction;
import com.haulmont.cuba.core.app.DataService;
import com.haulmont.cuba.core.entity.Server;
import com.haulmont.cuba.core.global.*;
import com.haulmont.cuba.security.app.LoginWorker;
import com.haulmont.cuba.security.entity.*;
import com.haulmont.cuba.security.global.UserSession;
import com.haulmont.cuba.testsupport.TestContainer;
import com.haulmont.cuba.testsupport.TestUserSessionSource;
import org.junit.After;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Test;
import java.util.List;
import java.util.Locale;
import static org.junit.Assert.*;
public class DataManagerSecurityTest {
@ClassRule
public static TestContainer cont = TestContainer.Common.INSTANCE;
private static final String USER_NAME = "testUser";
private static final String USER_PASSW = "testUser";
private static final String PERM_TARGET = "sys$Server:" + EntityOp.READ.getId();
private PasswordEncryption passwordEncryption;
private Role role;
private Permission permission;
private Group group;
private User user;
private UserRole userRole;
private Server server;
@Before
public void setUp() throws Exception {
passwordEncryption = AppBeans.get(PasswordEncryption.class);
try (Transaction tx = cont.persistence().createTransaction()) {
EntityManager em = cont.persistence().getEntityManager();
server = new Server();
server.setName("someServer");
server.setRunning(false);
em.persist(server);
role = new Role();
role.setName("testRole1");
em.persist(role);
permission = new Permission();
permission.setRole(role);
permission.setType(PermissionType.ENTITY_OP);
permission.setTarget(PERM_TARGET);
permission.setValue(0);
em.persist(permission);
group = new Group();
group.setName("testGroup");
em.persist(group);
user = new User();
user.setName(USER_NAME);
user.setLogin(USER_NAME);
String pwd = passwordEncryption.getPasswordHash(user.getId(), USER_PASSW);
user.setPassword(pwd);
user.setGroup(group);
em.persist(user);
userRole = new UserRole();
userRole.setUser(user);
userRole.setRole(role);
em.persist(userRole);
tx.commit();
}
}
@After
public void tearDown() throws Exception {
cont.deleteRecord(userRole, user, group, permission, role, server);
}
@Test
public void test() throws Exception {
LoginWorker lw = AppBeans.get(LoginWorker.NAME);
UserSession userSession = lw.login(USER_NAME, passwordEncryption.getPlainHash(USER_PASSW), Locale.getDefault());
assertNotNull(userSession);
UserSessionSource uss = AppBeans.get(UserSessionSource.class);
UserSession savedUserSession = uss.getUserSession();
((TestUserSessionSource) uss).setUserSession(userSession);
try {
DataManager dm = AppBeans.get(DataManager.NAME);
LoadContext<Server> loadContext = LoadContext.create(Server.class)
.setQuery(new LoadContext.Query("select s from sys$Server s"));
List<Server> list = dm.loadList(loadContext);
assertFalse("Permission took effect when calling DataManager inside middleware", list.isEmpty());
DataService ds = AppBeans.get(DataService.NAME);
loadContext = LoadContext.create(Server.class)
.setQuery(new LoadContext.Query("select s from sys$Server s"));
list = ds.loadList(loadContext);
assertTrue("Permission did not take effect when calling DataService", list.isEmpty());
} finally {
((TestUserSessionSource) uss).setUserSession(savedUserSession);
}
}
}

View File

@ -18,6 +18,11 @@ import java.util.Set;
* <p>Works with non-managed (new or detached) entities, always starts and commits new transactions. Can be used on
* both middle and client tiers.</p>
*
* <p>When used on the client tier - always applies security restrictions.
* <p>When used on the middleware - does not apply security restrictions by default. If you want to apply security,
* get {@link #secure()} instance or set the {@code cuba.dataManagerChecksSecurityOnMiddleware} application property
* to use it by default.
*
* @author krivopustov
* @version $Id$
*/
@ -127,4 +132,14 @@ public interface DataManager {
* @param entity entity instance
*/
void remove(Entity entity);
/**
* Returns the DataManager implementation that is guaranteed to apply security restrictions.
* <p>By default, DataManager does not apply security when used on the middleware. Use this method if you want
* to run the same code both on the client and middle tier. For example:
* <pre>
* AppBeans.get(DataManager.class).secure().load(context);
* </pre>
*/
DataManager secure();
}

View File

@ -34,6 +34,8 @@ public class SecurityContext {
private UserSession session;
private String user;
private boolean authorizationRequired;
public SecurityContext(UUID sessionId) {
Objects.requireNonNull(sessionId, "sessionId is null");
this.sessionId = sessionId;
@ -76,6 +78,31 @@ public class SecurityContext {
return user;
}
/**
* @return Whether the security check is required for standard mechanisms ({@code DataManager} in particular) on
* the middleware
*/
public boolean isAuthorizationRequired() {
return authorizationRequired;
}
/**
* Whether the security check is required for standard mechanisms ({@code DataManager} in particular) on
* the middleware. Example usage:
* <pre>
* boolean saved = AppContext.getSecurityContext().isAuthorizationRequired();
* AppContext.getSecurityContext().setAuthorizationRequired(true);
* try {
* // all calls to DataManager will apply security restrictions
* } finally {
* AppContext.getSecurityContext().setAuthorizationRequired(saved);
* }
* </pre>
*/
public void setAuthorizationRequired(boolean authorizationRequired) {
this.authorizationRequired = authorizationRequired;
}
@Override
public String toString() {
return "SecurityContext{" +

View File

@ -70,6 +70,11 @@ public class GenericDataSupplier implements DataSupplier {
dataManager.remove(entity);
}
@Override
public DataManager secure() {
return dataManager;
}
@Override
public Set<Entity> commit(CommitContext context) {
return dataManager.commit(context);