diff --git a/modules/client/src/com/haulmont/cuba/client/sys/DataManagerClientImpl.java b/modules/client/src/com/haulmont/cuba/client/sys/DataManagerClientImpl.java index 6c5aaee95b..8b32b3ff65 100644 --- a/modules/client/src/com/haulmont/cuba/client/sys/DataManagerClientImpl.java +++ b/modules/client/src/com/haulmont/cuba/client/sys/DataManagerClientImpl.java @@ -122,4 +122,9 @@ public class DataManagerClientImpl implements DataManager { Collections.singleton(entity)); commit(context); } + + @Override + public DataManager secure() { + return this; + } } diff --git a/modules/core/src/com/haulmont/cuba/core/app/DataManagerBean.java b/modules/core/src/com/haulmont/cuba/core/app/DataManagerBean.java index 3bf4b20fbb..8aaabc135c 100644 --- a/modules/core/src/com/haulmont/cuba/core/app/DataManagerBean.java +++ b/modules/core/src/com/haulmont/cuba/core/app/DataManagerBean.java @@ -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); + } + } + } } diff --git a/modules/core/src/com/haulmont/cuba/core/app/DataServiceBean.java b/modules/core/src/com/haulmont/cuba/core/app/DataServiceBean.java index bc2ea00e3d..f941680997 100644 --- a/modules/core/src/com/haulmont/cuba/core/app/DataServiceBean.java +++ b/modules/core/src/com/haulmont/cuba/core/app/DataServiceBean.java @@ -27,22 +27,22 @@ public class DataServiceBean implements DataService { @Override public Set commit(CommitContext context) { - return dataManager.commit(context); + return dataManager.secure().commit(context); } @Override @Nullable public E load(LoadContext context) { - return dataManager.load(context); + return dataManager.secure().load(context); } @Override public List loadList(LoadContext context) { - return dataManager.loadList(context); + return dataManager.secure().loadList(context); } @Override public long getCount(LoadContext context) { - return dataManager.getCount(context); + return dataManager.secure().getCount(context); } } \ No newline at end of file diff --git a/modules/core/src/com/haulmont/cuba/core/app/ServerConfig.java b/modules/core/src/com/haulmont/cuba/core/app/ServerConfig.java index ca32b314ab..9b34b451d9 100644 --- a/modules/core/src/com/haulmont/cuba/core/app/ServerConfig.java +++ b/modules/core/src/com/haulmont/cuba/core/app/ServerConfig.java @@ -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(); } diff --git a/modules/core/src/com/haulmont/cuba/core/sys/listener/EntityListenerManager.java b/modules/core/src/com/haulmont/cuba/core/sys/listener/EntityListenerManager.java index 0ad9e1e5f5..06b17cfc53 100644 --- a/modules/core/src/com/haulmont/cuba/core/sys/listener/EntityListenerManager.java +++ b/modules/core/src/com/haulmont/cuba/core/sys/listener/EntityListenerManager.java @@ -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); } } } diff --git a/modules/core/test/com/haulmont/cuba/security/DataManagerSecurityTest.java b/modules/core/test/com/haulmont/cuba/security/DataManagerSecurityTest.java new file mode 100644 index 0000000000..0f93c511bf --- /dev/null +++ b/modules/core/test/com/haulmont/cuba/security/DataManagerSecurityTest.java @@ -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 loadContext = LoadContext.create(Server.class) + .setQuery(new LoadContext.Query("select s from sys$Server s")); + List 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); + } + } +} diff --git a/modules/global/src/com/haulmont/cuba/core/global/DataManager.java b/modules/global/src/com/haulmont/cuba/core/global/DataManager.java index 107756c927..964cfc4eaf 100644 --- a/modules/global/src/com/haulmont/cuba/core/global/DataManager.java +++ b/modules/global/src/com/haulmont/cuba/core/global/DataManager.java @@ -18,6 +18,11 @@ import java.util.Set; *

Works with non-managed (new or detached) entities, always starts and commits new transactions. Can be used on * both middle and client tiers.

* + *

When used on the client tier - always applies security restrictions. + *

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. + *

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: + *

+     *     AppBeans.get(DataManager.class).secure().load(context);
+     * 
+ */ + DataManager secure(); } diff --git a/modules/global/src/com/haulmont/cuba/core/sys/SecurityContext.java b/modules/global/src/com/haulmont/cuba/core/sys/SecurityContext.java index df3b8772d8..07bf73be66 100644 --- a/modules/global/src/com/haulmont/cuba/core/sys/SecurityContext.java +++ b/modules/global/src/com/haulmont/cuba/core/sys/SecurityContext.java @@ -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: + *
+     * boolean saved = AppContext.getSecurityContext().isAuthorizationRequired();
+     * AppContext.getSecurityContext().setAuthorizationRequired(true);
+     * try {
+     *     // all calls to DataManager will apply security restrictions
+     * } finally {
+     *     AppContext.getSecurityContext().setAuthorizationRequired(saved);
+     * }
+     * 
+ */ + public void setAuthorizationRequired(boolean authorizationRequired) { + this.authorizationRequired = authorizationRequired; + } + @Override public String toString() { return "SecurityContext{" + diff --git a/modules/gui/src/com/haulmont/cuba/gui/data/impl/GenericDataSupplier.java b/modules/gui/src/com/haulmont/cuba/gui/data/impl/GenericDataSupplier.java index d1551ebff7..5401a4ee01 100644 --- a/modules/gui/src/com/haulmont/cuba/gui/data/impl/GenericDataSupplier.java +++ b/modules/gui/src/com/haulmont/cuba/gui/data/impl/GenericDataSupplier.java @@ -70,6 +70,11 @@ public class GenericDataSupplier implements DataSupplier { dataManager.remove(entity); } + @Override + public DataManager secure() { + return dataManager; + } + @Override public Set commit(CommitContext context) { return dataManager.commit(context);