diff --git a/CHANGELOG.md b/CHANGELOG.md index d895df1..3dde4ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,12 @@ # Changelog +## [v2.7.1-SNAPSHOT] - 2022-09-14 + +- - Feature #23847 Social service: temporarily block of notifications for given username(s) + ## [v2.7.0] - 2022-09-12 -- Sphinx documentation added + - Sphinx documentation added ## [v2.6.2] - 2022-07-27 diff --git a/pom.xml b/pom.xml index 28ea8e3..b9995d9 100644 --- a/pom.xml +++ b/pom.xml @@ -12,7 +12,7 @@ org.gcube.portal social-networking-library-ws war - 2.7.0 + 2.7.1-SNAPSHOT] social-networking-library-ws Rest interface for the social networking library. @@ -39,6 +39,11 @@ pom import + + org.aspectj + aspectjrt + 1.8.2 + @@ -73,6 +78,16 @@ ehcache 2.10.0 + + net.spy + spymemcached + 2.12.3 + + + org.gcube.common + authorization-control-library + [1.0.1,2.0.0-SNAPSHOT) + org.gcube.common.portal portal-manager @@ -301,7 +316,6 @@ - kr.motd.maven sphinx-maven-plugin @@ -348,7 +362,29 @@ - + + org.codehaus.mojo + aspectj-maven-plugin + 1.7 + + 1.8 + 1.8 + 1.8 + + + org.gcube.common + authorization-control-library + + + + + + + compile + + + + org.apache.maven.plugins diff --git a/src/main/java/org/gcube/portal/social/networking/ws/ex/AuthException.java b/src/main/java/org/gcube/portal/social/networking/ws/ex/AuthException.java new file mode 100644 index 0000000..e1ebdd3 --- /dev/null +++ b/src/main/java/org/gcube/portal/social/networking/ws/ex/AuthException.java @@ -0,0 +1,16 @@ +package org.gcube.portal.social.networking.ws.ex; + +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Response.Status; + +public class AuthException extends WebApplicationException { + /** + * + */ + private static final long serialVersionUID = 1L; + + public AuthException(Throwable cause) { + super(cause, Status.FORBIDDEN); + } + +} \ No newline at end of file diff --git a/src/main/java/org/gcube/portal/social/networking/ws/methods/v2/Notifications.java b/src/main/java/org/gcube/portal/social/networking/ws/methods/v2/Notifications.java index 61fc7fe..07f6546 100644 --- a/src/main/java/org/gcube/portal/social/networking/ws/methods/v2/Notifications.java +++ b/src/main/java/org/gcube/portal/social/networking/ws/methods/v2/Notifications.java @@ -26,6 +26,7 @@ import org.gcube.applicationsupportlayer.social.NotificationsManager; import org.gcube.applicationsupportlayer.social.ScopeBeanExt; import org.gcube.applicationsupportlayer.social.shared.SocialNetworkingSite; import org.gcube.applicationsupportlayer.social.shared.SocialNetworkingUser; +import org.gcube.common.authorization.control.annotations.AuthorizationControl; import org.gcube.common.authorization.library.provider.AuthorizationProvider; import org.gcube.common.authorization.library.utils.Caller; import org.gcube.common.scope.api.ScopeProvider; @@ -40,11 +41,13 @@ import org.gcube.portal.social.networking.caches.UsersCache; import org.gcube.portal.social.networking.liferay.ws.GroupManagerWSBuilder; import org.gcube.portal.social.networking.liferay.ws.LiferayJSONWsCredentials; import org.gcube.portal.social.networking.liferay.ws.UserManagerWSBuilder; +import org.gcube.portal.social.networking.ws.ex.AuthException; import org.gcube.portal.social.networking.ws.mappers.CatalogueEventTypeMapper; import org.gcube.portal.social.networking.ws.mappers.JobMapper; import org.gcube.portal.social.networking.ws.mappers.WorkspaceItemMapper; import org.gcube.portal.social.networking.ws.outputs.ResponseBean; import org.gcube.portal.social.networking.ws.utils.CassandraConnection; +import org.gcube.portal.social.networking.ws.utils.DistributedCacheClient; import org.gcube.portal.social.networking.ws.utils.ErrorMessages; import org.gcube.social_networking.socialnetworking.model.beans.JobNotificationBean; @@ -72,6 +75,8 @@ import com.webcohesion.enunciate.metadata.rs.RequestHeaders; import com.webcohesion.enunciate.metadata.rs.ResponseCode; import com.webcohesion.enunciate.metadata.rs.StatusCodes; +import net.spy.memcached.MemcachedClient; + /** * REST interface for the social networking library (notifications). */ @@ -84,6 +89,7 @@ public class Notifications { // Logger private static final org.slf4j.Logger logger = LoggerFactory.getLogger(Notifications.class); + private static final String INFRASTRUCTURE_MANAGER_ROLE = "Infrastructure-Manager"; @GET @Path("get-range-notifications/") @@ -130,6 +136,47 @@ public class Notifications { return Response.status(status).entity(responseBean).build(); } + @GET + @Path("is-user-disabled/") + @Produces(MediaType.APPLICATION_JSON) + /** + * Return whether the notifications for this user are enabled or not + * @param from must be greater or equal to 1, range[0, infinity] + * @param quantity quantity must be greater or equal to 0 + * @return notifications up to quantity + * @throws ValidationException + */ + @StatusCodes ({ + @ResponseCode ( code = 200, condition = "Users with silenced notifications are reported in the 'result' field of the returned object"), + @ResponseCode ( code = 500, condition = ErrorMessages.ERROR_IN_API_RESULT) + }) + @AuthorizationControl(allowedRoles={INFRASTRUCTURE_MANAGER_ROLE}, exception=AuthException.class) + public Response isUserDisabled(String usernameToCheck) throws ValidationException{ + ResponseBean responseBean = new ResponseBean(); + Status status = Status.OK; + try{ + Boolean userDisabled= !isNotificationEnabled(usernameToCheck); + responseBean.setResult(userDisabled); + responseBean.setSuccess(true); + logger.debug("are User " + usernameToCheck + " Notifications Disabled?"+userDisabled); + }catch(Exception e){ + logger.error("Unable to read whether the notifications for this user are enabled or not.", e); + responseBean.setMessage(e.getMessage()); + responseBean.setSuccess(false); + status = Status.INTERNAL_SERVER_ERROR; + } + + return Response.status(status).entity(responseBean).build(); + } + + private boolean isNotificationEnabled(String usernameToCheck) { + MemcachedClient entries = new DistributedCacheClient().getMemcachedClient(); + Boolean userEnabled = false; + if(entries.get(usernameToCheck) == null) + userEnabled = true; + return userEnabled; + } + /** * Send a JOB notification to a given recipient * @param job The job bean @@ -225,26 +272,28 @@ public class Notifications { if (! event.idsAsGroup()) { for (int i = 0; i < idsToNotify.length; i++) { String userIdToNotify = idsToNotify[i]; - String username2Notify = null; - try { - username2Notify = um.getUserByUsername(userIdToNotify).getUsername(); - } - catch (Exception e) { - status = Status.BAD_REQUEST; - logger.error("Username not found", e); - responseBean.setSuccess(false); - responseBean.setMessage("Username not found, got: " + userIdToNotify); - return Response.status(status).entity(responseBean).build(); - } - - deliveryResult = + if (isNotificationEnabled(userIdToNotify)) { + String username2Notify = null; + try { + username2Notify = um.getUserByUsername(userIdToNotify).getUsername(); + } + catch (Exception e) { + status = Status.BAD_REQUEST; + logger.error("Username not found", e); + responseBean.setSuccess(false); + responseBean.setMessage("Username not found, got: " + userIdToNotify); + return Response.status(status).entity(responseBean).build(); + } + + deliveryResult = nm.notifyCatalogueEvent( CatalogueEventTypeMapper.getType(event.getType()), username2Notify, event.getItemId(), event.getNotifyText(), event.getItemURL()); - + } + } } else { //the ids are contexts for (int i = 0; i < idsToNotify.length; i++) { @@ -261,13 +310,15 @@ public class Notifications { String[] userIdsToNotify = getUsernamesByContext(scope).toArray(new String[0]); //resolve the members for (int j = 0; j < userIdsToNotify.length; j++) { String userIdToNotify = userIdsToNotify[j]; - deliveryResult = - nm.notifyCatalogueEvent( - CatalogueEventTypeMapper.getType(event.getType()), - userIdToNotify, - event.getItemId(), - event.getNotifyText(), - event.getItemURL()); + if (isNotificationEnabled(userIdToNotify)) { + deliveryResult = + nm.notifyCatalogueEvent( + CatalogueEventTypeMapper.getType(event.getType()), + userIdToNotify, + event.getItemId(), + event.getNotifyText(), + event.getItemURL()); + } } } } @@ -338,18 +389,20 @@ public class Notifications { if (! event.idsAsGroup()) { for (int i = 0; i < idsToNotify.length; i++) { String userIdToNotify = idsToNotify[i]; - String username2Notify = ""; - try { - username2Notify = um.getUserByUsername(userIdToNotify).getUsername(); + if (isNotificationEnabled(userIdToNotify)) { + String username2Notify = ""; + try { + username2Notify = um.getUserByUsername(userIdToNotify).getUsername(); + } + catch (Exception e) { + status = Status.NOT_ACCEPTABLE; + logger.error("Username not found", e); + responseBean.setSuccess(false); + responseBean.setMessage("Username not found, received: " + userIdToNotify); + return Response.status(status).entity(responseBean).build(); + } + deliveryResult = notifyWorkspaceEvent(event, nm, username2Notify); } - catch (Exception e) { - status = Status.NOT_ACCEPTABLE; - logger.error("Username not found", e); - responseBean.setSuccess(false); - responseBean.setMessage("Username not found, received: " + userIdToNotify); - return Response.status(status).entity(responseBean).build(); - } - deliveryResult = notifyWorkspaceEvent(event, nm, username2Notify); } } else { //the ids are contexts for (int i = 0; i < idsToNotify.length; i++) { @@ -366,7 +419,8 @@ public class Notifications { String[] userIdsToNotify = getUsernamesByContext(scope).toArray(new String[0]); //resolve the members for (int j = 0; j < userIdsToNotify.length; j++) { String userIdToNotify = userIdsToNotify[j]; - deliveryResult = notifyWorkspaceEvent(event, nm, userIdToNotify); + if (isNotificationEnabled(userIdToNotify)) + deliveryResult = notifyWorkspaceEvent(event, nm, userIdToNotify); } } } diff --git a/src/main/java/org/gcube/portal/social/networking/ws/utils/DistributedCacheClient.java b/src/main/java/org/gcube/portal/social/networking/ws/utils/DistributedCacheClient.java new file mode 100644 index 0000000..38fb3b2 --- /dev/null +++ b/src/main/java/org/gcube/portal/social/networking/ws/utils/DistributedCacheClient.java @@ -0,0 +1,95 @@ +package org.gcube.portal.social.networking.ws.utils; + +import static org.gcube.resources.discovery.icclient.ICFactory.clientFor; +import static org.gcube.resources.discovery.icclient.ICFactory.queryFor; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.List; + +import org.gcube.common.resources.gcore.ServiceEndpoint; +import org.gcube.common.resources.gcore.ServiceEndpoint.AccessPoint; +import org.gcube.common.resources.gcore.utils.Group; +import org.gcube.common.scope.api.ScopeProvider; +import org.gcube.resources.discovery.client.api.DiscoveryClient; +import org.gcube.resources.discovery.client.queries.api.SimpleQuery; +import org.gcube.smartgears.ContextProvider; +import org.gcube.smartgears.context.application.ApplicationContext; +import org.slf4j.LoggerFactory; + +import net.spy.memcached.KetamaConnectionFactory; +import net.spy.memcached.MemcachedClient; + +/** + * @author Massimiliano Assante at ISTI-CNR + */ +public class DistributedCacheClient { + + // Logger + private static final org.slf4j.Logger logger = LoggerFactory.getLogger(DistributedCacheClient.class); + + private static final String MEMCACHED_RESOURCE_NAME = "Memcached"; + private static final String CATEGORY = "Database"; + + private MemcachedClient mClient; + + /** + * Build the singleton instance + */ + public DistributedCacheClient(){ + List addrs = discoverHostOfServiceEndpoint(); + try { + mClient = new MemcachedClient(new KetamaConnectionFactory(), addrs); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public MemcachedClient getMemcachedClient() { + return mClient; + } + + /** + * Retrieve endpoint resoruce from IS + * @return List of InetSocketAddresses + * @throws Exception + */ + private static List discoverHostOfServiceEndpoint(){ + + String currentScope = ScopeProvider.instance.get(); + ApplicationContext ctx = ContextProvider.get(); // get this info from SmartGears + String infrastructure = "/"+ctx.container().configuration().infrastructure(); + ScopeProvider.instance.set(infrastructure); + List toReturn = new ArrayList(); + try{ + SimpleQuery query = queryFor(ServiceEndpoint.class); + query.addCondition("$resource/Profile/Name/text() eq '"+ MEMCACHED_RESOURCE_NAME +"'"); + query.addCondition("$resource/Profile/Category/text() eq '"+ CATEGORY +"'"); + DiscoveryClient client = clientFor(ServiceEndpoint.class); + List ses = client.submit(query); + if (ses.isEmpty()) { + logger.error("There is no Memcached cluster having name: " + MEMCACHED_RESOURCE_NAME + " and Category " + CATEGORY + " on root context in this infrastructure: "); + return null; + } + for (ServiceEndpoint se : ses) { + Group aps = se.profile().accessPoints(); + for (AccessPoint ap : aps.asCollection()) { + String address = ap.address(); //e.g. socialnetworking-d-d4s.d4science.org:11211 + String[] splits = address.split(":"); + String hostname = splits[0]; + int port = Integer.parseInt(splits[1]); + toReturn.add(new InetSocketAddress(hostname, port)); + } + break; + } + } catch(Exception e){ + logger.error("Error while retrieving hosts for the Memcached cluster having name: " + MEMCACHED_RESOURCE_NAME + " and Category " + CATEGORY + " on root context"); + }finally{ + ScopeProvider.instance.set(currentScope); + } + ScopeProvider.instance.set(currentScope); + return toReturn; + } + +}