From e42794d220377f9bf51ff6b7e92454b1c8b0e17b Mon Sep 17 00:00:00 2001
From: Sinisa Veseli <sveseli@aps.anl.gov>
Date: Fri, 4 Dec 2015 22:04:09 +0000
Subject: [PATCH] added user interfaces and utilities that enable experiment
 data download from machines that have ssh access to the storage host

---
 bin/dm-download                               | 18 ++++
 doc/RELEASE_NOTES.txt                         |  2 +
 etc/dm.sudo-rules.template                    |  2 +-
 src/python/dm/common/plugins/__init__.py      |  0
 .../common/plugins/fileProcessorInterface.py  |  9 --
 .../dm/common/plugins/fileTransferPlugin.py   | 91 -------------------
 .../common/plugins/rsyncFileTransferPlugin.py | 18 ----
 .../dm/common/utility/configurationManager.py |  1 +
 .../utility/ldapLinuxPlatformUtility.py       | 12 ++-
 src/python/dm/common/utility/linuxUtility.py  | 13 ++-
 .../dm/common/utility/rsyncFileTransfer.py    | 35 +++++++
 .../dm/ds_web_service/api/fileRestApi.py      | 31 +++++++
 .../dm/ds_web_service/cli/addExperimentCli.py |  5 +-
 .../dm/ds_web_service/cli/downloadCli.py      | 46 ++++++++++
 .../cli/statExperimentFileCli.py              |  2 +-
 .../service/experimentRouteDescriptor.py      | 18 ++++
 .../service/experimentSessionController.py    | 22 +++++
 .../service/impl/experimentManager.py         | 21 ++++-
 .../impl/experimentSessionControllerImpl.py   |  5 +
 .../service/userRouteDescriptor.py            |  1 +
 20 files changed, 226 insertions(+), 126 deletions(-)
 create mode 100755 bin/dm-download
 delete mode 100644 src/python/dm/common/plugins/__init__.py
 delete mode 100755 src/python/dm/common/plugins/fileProcessorInterface.py
 delete mode 100755 src/python/dm/common/plugins/fileTransferPlugin.py
 delete mode 100755 src/python/dm/common/plugins/rsyncFileTransferPlugin.py
 create mode 100755 src/python/dm/common/utility/rsyncFileTransfer.py
 create mode 100755 src/python/dm/ds_web_service/cli/downloadCli.py

diff --git a/bin/dm-download b/bin/dm-download
new file mode 100755
index 00000000..b0d57dfc
--- /dev/null
+++ b/bin/dm-download
@@ -0,0 +1,18 @@
+#!/bin/sh
+
+# Run command
+
+if [ -z $DM_ROOT_DIR ]; then
+    cd `dirname $0` && myDir=`pwd`
+    setupFile=$myDir/../setup.sh
+    if [ ! -f $setupFile ]; then
+        echo "Cannot find setup file: $setupFile"
+        exit 1
+    fi
+    source $setupFile > /dev/null
+fi
+source dm_command_setup.sh
+
+eval "$DM_ROOT_DIR/src/python/dm/ds_web_service/cli/downloadCli.py $DM_COMMAND_ARGS"
+
+
diff --git a/doc/RELEASE_NOTES.txt b/doc/RELEASE_NOTES.txt
index e19e6eca..90d7d0d7 100644
--- a/doc/RELEASE_NOTES.txt
+++ b/doc/RELEASE_NOTES.txt
@@ -7,6 +7,8 @@ Release 0.7 ()
   simultaneously (required changes to DAQ service REST interfaces)
 - Enhanced start/stop DAQ and upload commands to use DM_FILE_SERVER_URL
   environment variable
+- Added user interfaces and utilities that enable experiment data download
+  from machines that have SSH access to the storage host
 
 Release 0.6 (11/6/2015)
 =============================
diff --git a/etc/dm.sudo-rules.template b/etc/dm.sudo-rules.template
index ad7f67d9..afc042be 100644
--- a/etc/dm.sudo-rules.template
+++ b/etc/dm.sudo-rules.template
@@ -6,7 +6,7 @@ Cmnd_Alias SETFACL=/usr/bin/setfacl -m group\:*\:rx DM_STORAGE_DIR/*
 Cmnd_Alias USERMOD=/usr/sbin/usermod -a -G * *
 Cmnd_Alias GROUPADD=/usr/sbin/groupadd *
 Cmnd_Alias CHOWN=/bin/chown -R \:* *
-Cmnd_Alias GPASSWD=/usr/bin/gpasswd -M * *
+Cmnd_Alias GPASSWD=/usr/bin/gpasswd * * *
 
 USER HOST = (root) NOPASSWD: SETFACL,USERMOD,GROUPADD,CHOWN,GPASSWD
 
diff --git a/src/python/dm/common/plugins/__init__.py b/src/python/dm/common/plugins/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/src/python/dm/common/plugins/fileProcessorInterface.py b/src/python/dm/common/plugins/fileProcessorInterface.py
deleted file mode 100755
index f9aef1ce..00000000
--- a/src/python/dm/common/plugins/fileProcessorInterface.py
+++ /dev/null
@@ -1,9 +0,0 @@
-#!/usr/bin/env python
-
-import abc
-class FileProcessorInterface:
-
-    @abc.abstractmethod
-    def processFile(self, filePath, daqPath, experiment):
-        return NotImplemented
-        
diff --git a/src/python/dm/common/plugins/fileTransferPlugin.py b/src/python/dm/common/plugins/fileTransferPlugin.py
deleted file mode 100755
index 1b369025..00000000
--- a/src/python/dm/common/plugins/fileTransferPlugin.py
+++ /dev/null
@@ -1,91 +0,0 @@
-#!/usr/bin/env python
-
-import os
-from dm.common.utility.loggingManager import LoggingManager
-from dm.common.utility.dmSubprocess import DmSubprocess
-from dm.common.exceptions.invalidArgument import InvalidArgument
-from dm.common.exceptions.invalidRequest import InvalidRequest
-from fileProcessorInterface import FileProcessorInterface
-
-class FileTransferPlugin(FileProcessorInterface):
-
-    def __init__(self, command, src=None, dest=None):
-        self.src = src
-        self.dest = dest
-        self.logger = LoggingManager.getInstance().getLogger(self.__class__.__name__)
-        if command is None or not len(command):
-            raise InvalidArgument('File transfer command must be non-empty string.')
-        self.command = command
-        self.subprocess = None
-
-    def processFile(self, filePath, daqPath, experiment):
-        storageHost = experiment.get('storageHost')
-        storageDirectory = experiment.get('storageDirectory')
-        dest = '%s:%s' % (storageHost, storageDirectory)
-        # Use relative path with respect to daq directory as a source
-        os.chdir(daqPath)
-        src = os.path.relpath(filePath, daqPath)
-        self.start(src, dest)
-
-    def getFullCommand(self, src, dest):
-        return '%s %s %s' % (self.command, src, dest)
-
-    def setSrc(self, src):
-        self.src = src
-
-    def setDest(self, dest):
-        self.dest = dest
-
-    def start(self, src=None, dest=None):
-        # Use preconfigured source if provided source is None
-        fileSrc = src
-        if src is None:
-            fileSrc = self.src
-        # Use provided destination only if preconfigured destination is None
-        # Plugins may have desired destination preconfigured for all files
-        fileDest = self.dest
-        if self.dest is None:
-            fileDest = dest
-
-        if not fileSrc or not fileDest:
-            raise InvalidRequest('Both source and destination must be non-empty strings.')
-        self.subprocess = DmSubprocess.getSubprocess(self.getFullCommand(fileSrc, fileDest))
-        return self.subprocess.run()
-
-    def wait(self):
-        if self.subprocess:
-            return self.subprocess.wait()
-        return None
-
-    def poll(self):
-        if self.subprocess:
-            return self.subprocess.poll()
-        return None
-
-    def getStdOut(self):
-        if self.subprocess:
-            return self.subprocess.getStdOut()
-        return None
-
-
-    def getStdErr(self):
-        if self.subprocess:
-            return self.subprocess.getStdErr()
-        return None
-
-    def getExitStatus(self):
-        if self.subprocess:
-            return self.subprocess.getExitStatus()
-        return None
-
-    def reset(self):
-        self.subprocess = None
-
-#######################################################################
-# Testing.
-if __name__ == '__main__':
-    ft = FileTransfer('rsync -arv', '/tmp/xyz', '/tmp/xyz2')
-    ft.start()
-    print 'StdOut: ', ft.getStdOut()
-    print 'StdErr: ', ft.getStdErr()
-    print 'Exit Status: ', ft.getExitStatus()
diff --git a/src/python/dm/common/plugins/rsyncFileTransferPlugin.py b/src/python/dm/common/plugins/rsyncFileTransferPlugin.py
deleted file mode 100755
index 3550d0cf..00000000
--- a/src/python/dm/common/plugins/rsyncFileTransferPlugin.py
+++ /dev/null
@@ -1,18 +0,0 @@
-#!/usr/bin/env python
-
-from fileTransferPlugin import FileTransferPlugin
-class RsyncFileTransferPlugin(FileTransferPlugin):
-
-    COMMAND = 'rsync -arvlPR'
-
-    def __init__(self, src=None, dest=None):
-        FileTransferPlugin.__init__(self, self.COMMAND, src, dest)
-
-#######################################################################
-# Testing.
-if __name__ == '__main__':
-    ft = RsyncFileTransferPlugin('/tmp/xyz', '/tmp/xyz2')
-    ft.start()
-    print 'StdOut: ', ft.getStdOut()
-    print 'StdErr: ', ft.getStdErr()
-    print 'Exit Status: ', ft.getExitStatus()
diff --git a/src/python/dm/common/utility/configurationManager.py b/src/python/dm/common/utility/configurationManager.py
index 55cf8e3c..075e9c9e 100755
--- a/src/python/dm/common/utility/configurationManager.py
+++ b/src/python/dm/common/utility/configurationManager.py
@@ -816,3 +816,4 @@ class ConfigurationManager(UserDict.UserDict):
 if __name__ == '__main__':
     cm = ConfigurationManager.getInstance()
     print cm
+    print cm.getUsername()
diff --git a/src/python/dm/common/utility/ldapLinuxPlatformUtility.py b/src/python/dm/common/utility/ldapLinuxPlatformUtility.py
index 354a6b70..4db48455 100755
--- a/src/python/dm/common/utility/ldapLinuxPlatformUtility.py
+++ b/src/python/dm/common/utility/ldapLinuxPlatformUtility.py
@@ -18,6 +18,7 @@ class LdapLinuxPlatformUtility:
     USERMOD_CMD = '/usr/sbin/usermod'
     SETFACL_CMD = '/usr/bin/setfacl'
     CHOWN_CMD = '/bin/chown'
+    GPASSWD_CMD = '/usr/bin/gpasswd'
 
     def __init__(self, serverUrl, adminDn, adminPasswordFile, groupDnFormat, minGidNumber=None):
         self.serverUrl = serverUrl
@@ -164,12 +165,21 @@ class LdapLinuxPlatformUtility:
             logger.error('Could not add user %s to group %s: %s' % (username, groupName, ex))
             raise InternalError(exception=ex)
 
+
     @classmethod
     def addLocalUserToGroup(cls, username, groupName):
         """ Add local user to group. """
         logger = cls.getLogger()
         logger.debug('Adding local user %s to group %s' % (username, groupName))
-        cmd = '%s -a -G %s %s' % (cls.USERMOD_CMD, groupName, username)
+        cmd = '%s -a %s %s' % (cls.GPASSWD_CMD, username, groupName)
+        cls.executeSudoCommand(cmd)
+
+    @classmethod
+    def deleteLocalUserFromGroup(cls, username, groupName):
+        """ Remove local user from group. """
+        logger = cls.getLogger()
+        logger.debug('Removing local user %s from group %s' % (username, groupName))
+        cmd = '%s -d %s %s' % (cls.GPASSWD_CMD, username, groupName)
         cls.executeSudoCommand(cmd)
 
     def getGroupInfo(self, groupName):
diff --git a/src/python/dm/common/utility/linuxUtility.py b/src/python/dm/common/utility/linuxUtility.py
index afe5c2a1..97acf654 100755
--- a/src/python/dm/common/utility/linuxUtility.py
+++ b/src/python/dm/common/utility/linuxUtility.py
@@ -49,7 +49,18 @@ class LinuxUtility:
     @classmethod
     def addLocalUserToGroup(cls, username, groupName):
         """ Add local user to group. """
-        cls.addUserToGroup(username, groupName)
+        logger = cls.getLogger()
+        logger.debug('Adding local user %s to group %s' % (username, groupName))
+        cmd = '%s -a %s %s' % (cls.GPASSWD_CMD, username, groupName)
+        cls.executeSudoCommand(cmd)
+
+    @classmethod
+    def deleteLocalUserFromGroup(cls, username, groupName):
+        """ Remove local user from group. """
+        logger = cls.getLogger()
+        logger.debug('Removing local user %s from group %s' % (username, groupName))
+        cmd = '%s -d %s %s' % (cls.GPASSWD_CMD, username, groupName)
+        cls.executeSudoCommand(cmd)
 
     @classmethod
     def setGroupUsers(cls, groupName, usernameList):
diff --git a/src/python/dm/common/utility/rsyncFileTransfer.py b/src/python/dm/common/utility/rsyncFileTransfer.py
new file mode 100755
index 00000000..c553aa5f
--- /dev/null
+++ b/src/python/dm/common/utility/rsyncFileTransfer.py
@@ -0,0 +1,35 @@
+#!/usr/bin/env python
+
+from dm.common.utility.dmSubprocess import DmSubprocess
+
+class RsyncFileTransfer:
+
+    COMMAND = 'rsync'
+
+    def __init__(self, src, dest, flags='-arvlP'):
+        self.src = src
+        self.dest = dest
+        self.flags = flags
+        self.command = '%s %s %s %s' % (self.COMMAND, self.flags, self.src, self.dest)
+        self.subprocess = DmSubprocess.getSubprocess(self.command)
+
+    def execute(self):
+        return self.subprocess.run()
+
+    def getStdOut(self):
+        return self.subprocess.getStdOut()
+
+    def getStdErr(self):
+        return self.subprocess.getStdErr()
+
+    def getExitStatus(self):
+        return self.subprocess.getExitStatus()
+
+#######################################################################
+# Testing.
+if __name__ == '__main__':
+    ft = RsyncFileTransfer('/tmp/abc', '/tmp/abc2')
+    ft.execute()
+    print 'StdOut: ', ft.getStdOut()
+    print 'StdErr: ', ft.getStdErr()
+    print 'Exit Status: ', ft.getExitStatus()
diff --git a/src/python/dm/ds_web_service/api/fileRestApi.py b/src/python/dm/ds_web_service/api/fileRestApi.py
index 597d886b..6d365118 100755
--- a/src/python/dm/ds_web_service/api/fileRestApi.py
+++ b/src/python/dm/ds_web_service/api/fileRestApi.py
@@ -3,10 +3,14 @@
 import os
 import urllib
 import json
+import getpass
 
 from dm.common.utility.encoder import Encoder
 from dm.common.exceptions.dmException import DmException
+from dm.common.exceptions.invalidRequest import InvalidRequest
 from dm.common.objects.fileMetadata import FileMetadata
+from dm.common.objects.experiment import Experiment
+from dm.common.utility.rsyncFileTransfer import RsyncFileTransfer
 from dsRestApi import DsRestApi
 
 class FileRestApi(DsRestApi):
@@ -40,6 +44,33 @@ class FileRestApi(DsRestApi):
         responseDict = self.sendSessionRequest(url=url, method='POST')
         return FileMetadata(responseDict)
 
+    @DsRestApi.execute
+    def download(self, experimentName, experimentFilePath='',  destDirectory='.'):
+        username = getpass.getuser()
+
+        # Initialize download
+        url = '%s/downloadAuthorizations/%s/%s' % (self.getContextRoot(), username, experimentName)
+        if not experimentName:
+            raise InvalidRequest('Experiment name must be provided.')
+        self.logger.info('Authorizing download for user %s (experiment: %s)' % (username, experimentName))
+        responseDict = self.sendSessionRequest(url=url, method='POST')
+        experiment = Experiment(responseDict)
+        storageDirectory = experiment.get('storageDirectory')
+        storageHost = experiment.get('storageHost')
+        src = '%s@%s:%s' % (username, storageHost, storageDirectory)
+        if experimentFilePath:
+            src = '%s/%s' % (src, experimentFilePath)
+        dest = destDirectory
+             
+        # Download
+        fileTransfer = RsyncFileTransfer(src=src, dest=dest)
+        self.logger.info('Executing file download on behalf of %s (experiment: %s)' % (username, experimentName))
+        fileTransfer.execute()
+
+        # Finalize download
+        self.logger.info('Deleting download authorization for user %s (experiment: %s)' % (username, experimentName))
+        self.sendSessionRequest(url=url, method='DELETE')
+
 #######################################################################
 # Testing.
 
diff --git a/src/python/dm/ds_web_service/cli/addExperimentCli.py b/src/python/dm/ds_web_service/cli/addExperimentCli.py
index 566a41d0..91a0dab5 100755
--- a/src/python/dm/ds_web_service/cli/addExperimentCli.py
+++ b/src/python/dm/ds_web_service/cli/addExperimentCli.py
@@ -10,8 +10,8 @@ class AddExperimentCli(DsWebServiceSessionCli):
         self.addOption('', '--experiment', dest='experimentName', help='Experiment name.')
         self.addOption('', '--type-id', dest='typeId', help='Experiment type id.')
         self.addOption('', '--description', dest='description', help='Experiment description.')
-        self.addOption('', '--start-date', dest='startDate', help='Experiment start date in format 31-AUG-15.')
-        self.addOption('', '--end-date', dest='endDate', help='Experiment end date in format 31-AUG-15.')
+        self.addOption('', '--start-date', dest='startDate', help='Experiment start date in format DD-MMM-YY.')
+        self.addOption('', '--end-date', dest='endDate', help='Experiment end date in format DD-MMM-YY.')
 
     def checkArgs(self):
         if self.options.experimentName is None:
@@ -19,7 +19,6 @@ class AddExperimentCli(DsWebServiceSessionCli):
         if self.options.typeId is None:
             raise InvalidRequest('Experiment type id must be provided.')
 
-
     def getExperimentName(self):
         return self.options.experimentName
 
diff --git a/src/python/dm/ds_web_service/cli/downloadCli.py b/src/python/dm/ds_web_service/cli/downloadCli.py
new file mode 100755
index 00000000..fe9650e6
--- /dev/null
+++ b/src/python/dm/ds_web_service/cli/downloadCli.py
@@ -0,0 +1,46 @@
+#!/usr/bin/env python
+
+from dm.common.exceptions.invalidRequest import InvalidRequest
+from dm.ds_web_service.api.fileRestApi import FileRestApi
+from dsWebServiceSessionCli import DsWebServiceSessionCli
+
+class DownloadCli(DsWebServiceSessionCli):
+    def __init__(self):
+        DsWebServiceSessionCli.__init__(self)
+        self.addOption('', '--experiment', dest='experimentName', help='Experiment name.')
+        self.addOption('', '--relative-path', dest='experimentFilePath', default='', help='Experiment (relative) file path. If omitted, all experiment data will be downloaded.')
+        self.addOption('', '--destination-directory', dest='destinationDirectory', default='.', help='Destination directory. If omitted, files will be downloaded to current directory.')
+
+    def checkArgs(self):
+        if self.options.experimentName is None:
+            raise InvalidRequest('Experiment name must be provided.')
+
+    def getExperimentName(self):
+        return self.options.experimentName
+
+    def getExperimentFilePath(self):
+        return self.options.experimentFilePath
+
+    def getDestinationDirectory(self):
+        return self.options.destinationDirectory
+
+    def runCommand(self):
+        self.parseArgs(usage="""
+    dm-download --experiment=EXPERIMENTNAME 
+        [--relative-path=EXPERIMENTFILEPATH]
+        [--destination-directory=DESTINATIONDIRECTORY]
+
+Description:
+    Downloads experiment files.
+        """)
+        self.checkArgs()
+        api = FileRestApi(self.getLoginUsername(), self.getLoginPassword(), self.getServiceHost(), self.getServicePort(), self.getServiceProtocol())
+        api.download(self.getExperimentName(), self.getExperimentFilePath(), self.getDestinationDirectory())
+        #print fileMetadata.getDisplayString(self.getDisplayKeys(), self.getDisplayFormat())
+
+#######################################################################
+# Run command.
+if __name__ == '__main__':
+    cli = DownloadCli()
+    cli.run()
+
diff --git a/src/python/dm/ds_web_service/cli/statExperimentFileCli.py b/src/python/dm/ds_web_service/cli/statExperimentFileCli.py
index 6e0565a3..f56798df 100755
--- a/src/python/dm/ds_web_service/cli/statExperimentFileCli.py
+++ b/src/python/dm/ds_web_service/cli/statExperimentFileCli.py
@@ -24,7 +24,7 @@ class StatExperimentFileCli(DsWebServiceSessionCli):
 
     def runCommand(self):
         self.parseArgs(usage="""
-    dm-get-experiment --experiment=EXPERIMENTNAME --relative-path=EXPERIMENTFILEPATH
+    dm-stat-file --experiment=EXPERIMENTNAME --relative-path=EXPERIMENTFILEPATH
 
 Description:
     Retrieves stat information for a given file.
diff --git a/src/python/dm/ds_web_service/service/experimentRouteDescriptor.py b/src/python/dm/ds_web_service/service/experimentRouteDescriptor.py
index b599c4a6..f68d0f59 100755
--- a/src/python/dm/ds_web_service/service/experimentRouteDescriptor.py
+++ b/src/python/dm/ds_web_service/service/experimentRouteDescriptor.py
@@ -91,6 +91,24 @@ class ExperimentRouteDescriptor:
                 'method' : ['PUT']
             },
 
+            # Authorize download for a given UNIX username
+            {
+                'name' : 'authorizeDownload',
+                'path' : '%s/downloadAuthorizations/:(username)/:(experimentName)' % contextRoot,
+                'controller' : experimentSessionController,
+                'action' : 'authorizeDownload',
+                'method' : ['POST']
+            },
+
+            # Revoke authorization download for a given UNIX username
+            {
+                'name' : 'deauthorizeDownload',
+                'path' : '%s/downloadAuthorizations/:(username)/:(experimentName)' % contextRoot,
+                'controller' : experimentSessionController,
+                'action' : 'deauthorizeDownload',
+                'method' : ['DELETE']
+            },
+
         ]
        
         return routes
diff --git a/src/python/dm/ds_web_service/service/experimentSessionController.py b/src/python/dm/ds_web_service/service/experimentSessionController.py
index 5192b3f7..64d983ec 100755
--- a/src/python/dm/ds_web_service/service/experimentSessionController.py
+++ b/src/python/dm/ds_web_service/service/experimentSessionController.py
@@ -103,3 +103,25 @@ class ExperimentSessionController(DmSessionController):
         self.logger.debug('Returning: %s' % response)
         return response
 
+    @cherrypy.expose
+    @DmSessionController.require(DmSessionController.isAdministrator())
+    @DmSessionController.execute
+    def authorizeDownload(self, username, experimentName, **kwargs):
+        if not username:
+            raise InvalidRequest('Invalid username provided.')
+        if not experimentName:
+            raise InvalidRequest('Invalid experiment name provided.')
+        response = self.experimentSessionControllerImpl.authorizeDownload(username, experimentName).getFullJsonRep()
+        return response
+
+    @cherrypy.expose
+    @DmSessionController.require(DmSessionController.isAdministrator())
+    @DmSessionController.execute
+    def deauthorizeDownload(self, username, experimentName, **kwargs):
+        if not username:
+            raise InvalidRequest('Invalid username provided.')
+        if not experimentName:
+            raise InvalidRequest('Invalid experiment name provided.')
+        response = self.experimentSessionControllerImpl.deauthorizeDownload(username, experimentName).getFullJsonRep()
+        return response
+
diff --git a/src/python/dm/ds_web_service/service/impl/experimentManager.py b/src/python/dm/ds_web_service/service/impl/experimentManager.py
index c44acf93..108b62ff 100755
--- a/src/python/dm/ds_web_service/service/impl/experimentManager.py
+++ b/src/python/dm/ds_web_service/service/impl/experimentManager.py
@@ -15,6 +15,7 @@ from dm.common.utility.fileUtility import FileUtility
 from dm.common.db.api.experimentDbApi import ExperimentDbApi
 from dm.common.processing.fileProcessingManager import FileProcessingManager
 from dm.common.exceptions.objectNotFound import ObjectNotFound
+from dm.common.exceptions.invalidRequest import InvalidRequest
 
 class ExperimentManager(Singleton):
 
@@ -52,7 +53,7 @@ class ExperimentManager(Singleton):
         cm = ConfigurationManager.getInstance()
         configItems = cm.getConfigItems(ExperimentManager.CONFIG_SECTION_NAME)
         self.logger.debug('Got config items: %s' % configItems)
-        self.storageDirectory =cm.getConfigOption(ExperimentManager.CONFIG_SECTION_NAME, ExperimentManager.STORAGE_DIRECTORY_KEY) 
+        self.storageDirectory = cm.getConfigOption(ExperimentManager.CONFIG_SECTION_NAME, ExperimentManager.STORAGE_DIRECTORY_KEY) 
         self.manageStoragePermissions = ValueUtility.toBoolean(cm.getConfigOption(ExperimentManager.CONFIG_SECTION_NAME, ExperimentManager.MANAGE_STORAGE_PERMISSIONS_KEY))
         platformUtility = cm.getConfigOption(ExperimentManager.CONFIG_SECTION_NAME, ExperimentManager.PLATFORM_UTILITY_KEY)
         if platformUtility:
@@ -90,6 +91,24 @@ class ExperimentManager(Singleton):
             experimentUsers = experiment.get('experimentUsernameList', [])
             self.platformUtility.setGroupUsers(experimentName, experimentUsers)
 
+    def authorizeDownload(self, username, experimentName):
+        experiment = self.experimentDbApi.getExperimentByName(experimentName)
+        storageDirectory = self.updateExperimentWithStorageDataDirectory(experiment)
+        if os.path.exists(storageDirectory):
+            self.platformUtility.addLocalUserToGroup(username, experimentName)
+        else:
+            raise InvalidRequest('Experiment %s has not been started.' % experimentName)
+        return experiment
+
+    def deauthorizeDownload(self, username, experimentName):
+        experiment = self.experimentDbApi.getExperimentByName(experimentName)
+        storageDirectory = self.updateExperimentWithStorageDataDirectory(experiment)
+        if os.path.exists(storageDirectory):
+            self.platformUtility.deleteLocalUserFromGroup(username, experimentName)
+        else:
+            raise InvalidRequest('Experiment %s has not been started.' % experimentName)
+        return experiment
+
     def createExperimentGroup(self, experiment):
         experimentName = experiment.get('name')
         storageDirectory = experiment.get('storageDirectory')
diff --git a/src/python/dm/ds_web_service/service/impl/experimentSessionControllerImpl.py b/src/python/dm/ds_web_service/service/impl/experimentSessionControllerImpl.py
index 05af7916..24b69ee9 100755
--- a/src/python/dm/ds_web_service/service/impl/experimentSessionControllerImpl.py
+++ b/src/python/dm/ds_web_service/service/impl/experimentSessionControllerImpl.py
@@ -62,3 +62,8 @@ class ExperimentSessionControllerImpl(DmObjectManager):
         ExperimentManager.getInstance().updateExperimentWithStorageDataDirectory(experiment)
         return experiment
 
+    def authorizeDownload(self, username, experimentName):
+        return ExperimentManager.getInstance().authorizeDownload(username, experimentName)
+
+    def deauthorizeDownload(self, username, experimentName):
+        return ExperimentManager.getInstance().deauthorizeDownload(username, experimentName)
diff --git a/src/python/dm/ds_web_service/service/userRouteDescriptor.py b/src/python/dm/ds_web_service/service/userRouteDescriptor.py
index cb1e7595..004b4739 100755
--- a/src/python/dm/ds_web_service/service/userRouteDescriptor.py
+++ b/src/python/dm/ds_web_service/service/userRouteDescriptor.py
@@ -63,6 +63,7 @@ class UserRouteDescriptor:
                 'action' : 'deleteUserExperimentRole',
                 'method' : ['DELETE']
             },
+
         ]
        
         return routes
-- 
GitLab