Index: kernel-impl/src/test/java/org/sakaiproject/content/impl/test/VirusScannerTest.java
===================================================================
--- kernel-impl/src/test/java/org/sakaiproject/content/impl/test/VirusScannerTest.java (revision 0)
+++ kernel-impl/src/test/java/org/sakaiproject/content/impl/test/VirusScannerTest.java (revision 0)
@@ -0,0 +1,84 @@
+package org.sakaiproject.content.impl.test;
+
+import junit.extensions.TestSetup;
+import junit.framework.Test;
+import junit.framework.TestSuite;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.junit.FixMethodOrder;
+import org.sakaiproject.content.api.ContentHostingService;
+import org.sakaiproject.content.api.ContentResource;
+import org.sakaiproject.content.api.ContentResourceEdit;
+import org.sakaiproject.content.impl.BaseContentService;
+import org.sakaiproject.exception.IdUnusedException;
+import org.sakaiproject.test.SakaiKernelTestBase;
+import org.sakaiproject.tool.api.Session;
+import org.sakaiproject.tool.api.SessionManager;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+
+import static org.junit.runners.MethodSorters.NAME_ASCENDING;
+
+/**
+ * Created with IntelliJ IDEA.
+ * User: jbush
+ * Date: 9/16/13
+ * Time: 1:32 PM
+ * To change this template use File | Settings | File Templates.
+ */
+@FixMethodOrder(NAME_ASCENDING)
+public class VirusScannerTest extends SakaiKernelTestBase {
+
+ private static final String SIMPLE_FOLDER1 = "/admin/folder1/";
+ private static final Log log = LogFactory.getLog(VirusScannerTest.class);
+
+
+ public static Test suite()
+ {
+ TestSetup setup = new TestSetup(new TestSuite(VirusScannerTest.class))
+ {
+ protected void setUp() throws Exception
+ {
+ log.debug("starting oneTimeSetup");
+ oneTimeSetup("antivirus");
+ log.debug("finished oneTimeSetup");
+ }
+ protected void tearDown() throws Exception
+ {
+ log.debug("starting tearDown");
+ oneTimeTearDown();
+ log.debug("finished tearDown");
+ }
+ };
+ return setup;
+ }
+
+
+ /**
+ * Checks the resources of zero bytes are handled correctly.
+ */
+ public void testVirusFound() throws Exception {
+ ContentHostingService ch = getService(ContentHostingService.class);
+ SessionManager sm = getService(SessionManager.class);
+ Session session = sm.getCurrentSession();
+ session.setUserEid("admin");
+ session.setUserId("admin");
+ ContentResourceEdit cr;
+ cr = ch.addResource("/fileStream1");
+ cr.setContent(new ByteArrayInputStream("test".getBytes()));
+ ch.commitResource(cr);
+
+
+ BaseContentService bchs = (BaseContentService)ch;
+ bchs.processVirusQueue();
+
+ try {
+ ContentResource resource = ch.getResource("/fileStream1");
+ } catch (IdUnusedException e) {
+ assertTrue("file not found, this is expected because a virus was detected", true);
+ return;
+ }
+ assertTrue("the file was found, this is not expected, since a virus was found it should have been removed", false);
+ }
+}
\ No newline at end of file
Property changes on: kernel-impl/src/test/java/org/sakaiproject/content/impl/test/VirusScannerTest.java
___________________________________________________________________
Added: svn:keywords
+ Date Revision Author HeadURL Id
Added: svn:eol-style
+ native
Index: kernel-impl/src/test/java/org/sakaiproject/content/impl/test/MockVirusScanner.java
===================================================================
--- kernel-impl/src/test/java/org/sakaiproject/content/impl/test/MockVirusScanner.java (revision 0)
+++ kernel-impl/src/test/java/org/sakaiproject/content/impl/test/MockVirusScanner.java (revision 0)
@@ -0,0 +1,33 @@
+package org.sakaiproject.content.impl.test;
+
+import org.mockito.Mockito;
+import org.sakaiproject.antivirus.api.VirusFoundException;
+import org.sakaiproject.antivirus.api.VirusScanner;
+
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.when;
+
+/**
+ * Created with IntelliJ IDEA.
+ * User: jbush
+ * Date: 9/16/13
+ * Time: 1:06 PM
+ * To change this template use File | Settings | File Templates.
+ */
+public class MockVirusScanner {
+
+ static public VirusScanner virusScannerFound() {
+ VirusScanner virusScanner = Mockito.mock(VirusScanner.class);
+ when(virusScanner.getEnabled()).thenReturn(true);
+ doThrow(new VirusFoundException("virus found")).when(virusScanner).scanContent(anyString());
+ return virusScanner;
+ }
+
+ static public VirusScanner virusScannerNotFound() {
+ VirusScanner virusScanner = Mockito.mock(VirusScanner.class);
+ doNothing().when(virusScanner).scanContent(anyString());
+ return virusScanner;
+ }
+}
Property changes on: kernel-impl/src/test/java/org/sakaiproject/content/impl/test/MockVirusScanner.java
___________________________________________________________________
Added: svn:keywords
+ Date Revision Author HeadURL Id
Added: svn:eol-style
+ native
Index: kernel-impl/src/test/resources/antivirus/sakai-configuration.xml
===================================================================
--- kernel-impl/src/test/resources/antivirus/sakai-configuration.xml (revision 0)
+++ kernel-impl/src/test/resources/antivirus/sakai-configuration.xml (revision 0)
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
Property changes on: kernel-impl/src/test/resources/antivirus/sakai-configuration.xml
___________________________________________________________________
Added: svn:eol-style
+ native
Index: kernel-impl/src/main/java/org/sakaiproject/content/impl/BaseContentService.java
===================================================================
--- kernel-impl/src/main/java/org/sakaiproject/content/impl/BaseContentService.java (revision 129741)
+++ kernel-impl/src/main/java/org/sakaiproject/content/impl/BaseContentService.java (working copy)
@@ -42,12 +42,15 @@
import java.util.Iterator;
import java.util.List;
import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Random;
import java.util.Set;
import java.util.SortedSet;
import java.util.Stack;
import java.util.StringTokenizer;
+import java.util.Timer;
+import java.util.TimerTask;
import java.util.TreeSet;
-import java.util.Map.Entry;
import java.util.Vector;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
@@ -63,6 +66,7 @@
import org.apache.commons.logging.LogFactory;
import org.sakaiproject.alias.api.AliasService;
import org.sakaiproject.antivirus.api.VirusFoundException;
+import org.sakaiproject.antivirus.api.VirusScanIncompleteException;
import org.sakaiproject.antivirus.api.VirusScanner;
import org.sakaiproject.authz.api.AuthzGroup;
import org.sakaiproject.authz.api.AuthzGroupService;
@@ -71,6 +75,7 @@
import org.sakaiproject.authz.api.GroupNotDefinedException;
import org.sakaiproject.authz.api.Role;
import org.sakaiproject.authz.api.RoleAlreadyDefinedException;
+import org.sakaiproject.authz.api.SecurityAdvisor;
import org.sakaiproject.authz.api.SecurityService;
import org.sakaiproject.component.api.ServerConfigurationService;
import org.sakaiproject.conditions.api.ConditionService;
@@ -142,6 +147,7 @@
import org.sakaiproject.thread_local.api.ThreadLocalManager;
import org.sakaiproject.time.api.Time;
import org.sakaiproject.time.api.TimeService;
+import org.sakaiproject.tool.api.Session;
import org.sakaiproject.tool.api.SessionBindingEvent;
import org.sakaiproject.tool.api.SessionBindingListener;
import org.sakaiproject.tool.api.SessionManager;
@@ -204,6 +210,21 @@
protected static final String DEFAULT_RESOURCE_QUOTA = "content.quota.";
protected static final String DEFAULT_DROPBOX_QUOTA = "content.dropbox.quota.";
+ /**
+ * This is the name of the sakai.properties property for the VIRUS_SCAN_PERIOD,
+ * this is how long (in seconds) the virus scan service will wait between checking to see if there
+ * is new content that need to be scanned, default=3600
+ */
+ public static final String VIRUS_SCAN_CHECK_PERIOD_PROPERTY = "virus.scan.check.period";
+
+ /**
+ * This is the name of the sakai.properties property for the VIRUS_SCAN_DELAY,
+ * this is how long (in seconds) the virus scan service will wait after starting up
+ * before it does the first check for scanning, default=300
+ */
+ public static final String VIRUS_SCAN_START_DELAY_PROPERTY = "virus.scan.start.delay";
+
+
/** The initial portion of a relative access point URL. */
protected String m_relativeAccessPoint = null;
@@ -250,6 +271,39 @@
/** Dependency: MemoryService. */
protected MemoryService m_memoryService = null;
+ /**
+ * Use a timer for repeating actions
+ */
+ private Timer virusScanTimer = new Timer(true);
+
+ /** How long to wait between virus scan checks (seconds) */
+ private int VIRUS_SCAN_PERIOD = 300;
+
+ /** How long to wait between virus scan checks (seconds) */
+ public void setVIRUS_SCAN_PERIOD(int scan_period) {
+ VIRUS_SCAN_PERIOD = scan_period;
+ }
+
+ /** How long to wait before the first virus scan check (seconds) */
+ private int VIRUS_SCAN_DELAY = 300;
+ /** How long to wait before the first virus scan check (seconds) */
+ public void setVIRUS_SCAN_DELAY(int virus_scan_delay) {
+ VIRUS_SCAN_DELAY = virus_scan_delay;
+ }
+
+ private List virusScanQueue = new Vector();
+
+ private final static SecurityAdvisor ALLOW_ADVISOR;
+
+ static {
+ ALLOW_ADVISOR = new SecurityAdvisor(){
+ public SecurityAdvice isAllowed(String userId, String function, String reference)
+ {
+ return SecurityAdvice.ALLOWED;
+ }
+ };
+ }
+
/**
* Dependency: MemoryService.
*
@@ -849,6 +903,13 @@
m_dropBoxQuota = Long.parseLong(m_serverConfigurationService.getString("content.dropbox.quota", Long.toString(m_dropBoxQuota)));
M_log.info("init(): site quota: " + m_siteQuota + ", dropbox quota: " + m_dropBoxQuota + ", body path: " + m_bodyPath + " volumes: "+ buf.toString());
+
+ int virusScanPeriod = m_serverConfigurationService.getInt(VIRUS_SCAN_CHECK_PERIOD_PROPERTY, VIRUS_SCAN_PERIOD);
+ int virusScanDelay = m_serverConfigurationService.getInt(VIRUS_SCAN_START_DELAY_PROPERTY, VIRUS_SCAN_DELAY);
+
+ virusScanDelay += new Random().nextInt(60); // add some random delay to get the servers out of sync
+ virusScanTimer.schedule(new VirusTimerTask(), (virusScanDelay * 1000), (virusScanPeriod * 1000) );
+
}
catch (Exception t)
{
@@ -5512,34 +5573,12 @@
commitResourceEdit(edit, priority);
- if (virusScanner.getEnabled()) {
- try {
- virusScanner.scanContent(edit.getId());
- }
- catch (VirusFoundException e) {
- //this file is infected we need to remove if
- try {
- //the edit is closed so we need to refetch it
- ContentResourceEdit edit2 = editResource(edit.getId());
- removeResource(edit2);
- } catch (PermissionException e1) {
- // we're unlikely to see this at this point
- e1.printStackTrace();
- } catch (IdUnusedException e1) {
- // we're unlikely to see this at this point
- e1.printStackTrace();
- } catch (TypeException e1) {
- // we're unlikely to see this at this point
- e1.printStackTrace();
- } catch (InUseException e1) {
- // we're unlikely to see this at this point
- e1.printStackTrace();
- }
- throw e;
- }
- }
+ // Queue up content for virus scanning
+ if (virusScanner.getEnabled()) {
+ virusScanQueue.add(edit.getId());
+ }
- /**
+ /**
* check for over quota.
* We do this after the commit so we can actual tell its size
*/
@@ -5573,6 +5612,69 @@
} // commitResource
+ /**
+ * This timer task is run by the timer thread based on the period set above
+ */
+ protected class VirusTimerTask extends TimerTask {
+ public void run() {
+ try {
+ M_log.debug("running timer task");
+ enableAzgSecurityAdvisor();
+ processVirusQueue();
+ } catch (Exception e) {
+ M_log.error("Virus scan failure: " + e.getMessage(), e);
+ } finally {
+ disableAzgSecurityAdvisor();
+ }
+ }
+ }
+
+ public void processVirusQueue() {
+ // grab the queue - any new stuff will be processed next time
+ List queue = new Vector();
+ synchronized (virusScanQueue)
+ {
+ queue.addAll(virusScanQueue);
+ virusScanQueue.clear();
+ }
+
+ Session session = sessionManager.getCurrentSession();
+
+ for (String contentId : queue) {
+ // process the queue of digest requests
+ try {
+ virusScanner.scanContent(contentId);
+ } catch (VirusFoundException e) {
+ //this file is infected we need to remove if
+ try {
+ //the edit is closed so we need to refetch it
+ ContentResourceEdit edit2 = editResource(contentId);
+ ResourceProperties props = edit2.getProperties();
+ // we need to set the session userId or removeResource fails
+ String owner = props.getProperty(ResourceProperties.PROP_CREATOR);
+ User user = userDirectoryService.getUser(owner);
+ session.setUserEid(user.getEid());
+ session.setUserId(user.getId());
+ removeResource(edit2);
+ } catch (PermissionException e1) {
+ M_log.error(e1.getMessage(), e1);
+ } catch (IdUnusedException e1) {
+ M_log.error(e1.getMessage(), e1);
+ } catch (TypeException e1) {
+ M_log.error(e1.getMessage(), e1);
+ } catch (InUseException e1) {
+ M_log.error(e1.getMessage(), e1);
+ } catch (UserNotDefinedException e1) {
+ M_log.error(e1.getMessage(), e1);
+ }
+ } catch (VirusScanIncompleteException e1) {
+ M_log.info("virus scanning did not complete adding resource: " + contentId + " back to queue");
+ virusScanQueue.add(contentId);
+ }
+ }
+
+
+ }
private boolean checkUpdateContentEncoding(ContentResourceEdit edit) {
if (edit == null) {
return false;
@@ -13467,6 +13569,35 @@
throw exception;
}
}
+
+ /**
+ * Establish a security advisor to allow the "embedded" azg work to occur with no need for additional security permissions.
+ */
+ protected void enableAzgSecurityAdvisor()
+ {
+ // put in a security advisor so we can do our azg work without need of further permissions
+ // TODO: could make this more specific to the AuthzGroupService.SECURE_UPDATE_AUTHZ_GROUP permission -ggolden
+ m_securityService.pushAdvisor(ALLOW_ADVISOR);
+ }
+
+ /**
+ * Disabled the security advisor.
+ */
+ protected void disableAzgSecurityAdvisor()
+ {
+ SecurityAdvisor popped = m_securityService.popAdvisor(ALLOW_ADVISOR);
+ if (!ALLOW_ADVISOR.equals(popped)) {
+ if (popped == null)
+ {
+ M_log.warn("Someone has removed our advisor.");
+ }
+ else
+ {
+ M_log.warn("Removed someone elses advisor, adding it back.");
+ m_securityService.pushAdvisor(popped);
+ }
+ }
+ }
/**
* Expand the supplied resource under its parent collection.
Index: kernel-impl/pom.xml
===================================================================
--- kernel-impl/pom.xml (revision 129741)
+++ kernel-impl/pom.xml (working copy)
@@ -277,5 +277,17 @@
2.5.1
test
+
+ org.mockito
+ mockito-all
+ 1.9.5
+ test
+
+
+ org.sakaiproject.kernel
+ sakai-kernel-util
+ ${project.version}
+
+