Repository: TACC/agavepy

Issue:
Implement files_pems_update
**Is your feature request related to a problem? Please describe.**
Part of the changes proposed for release v3.0.0 of `TACC-Cloud/agave-cli`

**Describe the solution you'd like**
Implement method to edit user permissions on a file or folder. For all but iRODS systems, these permissions are set at the API level and do not reflect *nix or other file system ACL. On iRODS systems, if the system.storageConfig.mirror attribute is set to true then any permissions set via the API will be mirrored onto the remote system.

Discussion:

Patch:
diff --git a/agavepy/agave.py b/agavepy/agave.py
index 0cb8010..2d73cf2 100644
--- a/agavepy/agave.py
+++ b/agavepy/agave.py
@@ -26,7 +26,7 @@ from agavepy.tokens import token_create, refresh_token
 from agavepy.utils import load_config, save_config
 from agavepy.files import (files_copy, files_delete, files_download, 
     files_list, files_mkdir, files_move, files_pems_delete, files_pems_list, 
-    files_upload)
+    files_pems_update, files_upload)
 
 
 import sys
@@ -873,6 +873,25 @@ class Agave(object):
         files_pems_list(self.api_server, self.token, path)
 
 
+    def files_pems_update(self, path, username, perms, recursive=False):
+        """ Edit user permissions associated with a file or folder.
+        
+        These permissions are set at the API level and do not reflect *nix or 
+        other file system ACL.
+        Deletes all permissions on a file except those of the owner.
+        Valid values for setting permission with the -P flag are READ, WRITE, 
+        EXECUTE, READ_WRITE, READ_EXECUTE, WRITE_EXECUTE, ALL, and NONE.
+        """
+        # Check if tokens need to be refreshed.
+        self.refresh_tokens()
+
+        # Update api permissions.
+        files_pems_update(
+            self.api_server, self.token, 
+            path, username, perms, 
+            recursive=recursive)
+
+
     def files_upload(self, source, destination):
         """ Upload file to remote system
         """
diff --git a/agavepy/files/__init__.py b/agavepy/files/__init__.py
index b6b95cb..1daf53e 100644
--- a/agavepy/files/__init__.py
+++ b/agavepy/files/__init__.py
@@ -6,4 +6,5 @@ from .mkdir import files_mkdir
 from .move import files_move
 from .pems_delete import files_pems_delete
 from .pems_list import files_pems_list
+from .pems_update import files_pems_update
 from .upload import files_upload
diff --git a/agavepy/files/pems_delete.py b/agavepy/files/pems_delete.py
index 74ea6dc..b1b9dce 100644
--- a/agavepy/files/pems_delete.py
+++ b/agavepy/files/pems_delete.py
@@ -1,5 +1,5 @@
 """
-    pems_list.py
+    pems_delete.py
 """
 import requests
 from .exceptions import AgaveFilesError
diff --git a/agavepy/files/pems_update.py b/agavepy/files/pems_update.py
new file mode 100644
index 0000000..9ae319f
--- /dev/null
+++ b/agavepy/files/pems_update.py
@@ -0,0 +1,39 @@
+"""
+    pems_update.py
+"""
+import requests
+from .exceptions import AgaveFilesError
+from ..utils import handle_bad_response_status_code
+
+
+def files_pems_update(tenant_url, access_token, path, username, perms, recursive=False):
+    """ Edit user permissions associated with a file or folder. 
+    
+    These permissions are set at the API level and do not reflect *nix or other
+    file system ACL.
+    Deletes all permissions on a file except those of the owner.
+    Valid values for setting permission with the -P flag are READ, WRITE, 
+    EXECUTE, READ_WRITE, READ_EXECUTE, WRITE_EXECUTE, ALL, and NONE.
+    """
+    # Set request url.
+    endpoint = "{0}/{1}/{2}".format(tenant_url, "files/v2/pems/system", path)
+
+    # Obtain file path. "path" should include the system name at the begining,
+    # so we get rid of it.
+    destination = '/'.join( path.split('/')[1:] )
+
+    # Make request.
+    try:
+        headers  = {"Authorization":"Bearer {0}".format(access_token)}
+        data     = {
+            "username": username, 
+            "permission": perms, 
+            "recursive": recursive
+        }
+        params   = {"pretty": "true"}
+        resp = requests.post(endpoint, headers=headers, data=data, params=params)
+    except Exception as err:
+        raise AgaveFilesError(err)
+
+    # Handle bad status code.
+    handle_bad_response_status_code(resp)
diff --git a/docs/docsite/files/admin.rst b/docs/docsite/files/admin.rst
index ed7bde0..d120f34 100644
--- a/docs/docsite/files/admin.rst
+++ b/docs/docsite/files/admin.rst
@@ -19,3 +19,18 @@ To remove all the permissions on a file, except those of the owner:
 .. code-block:: pycon
 
     agave.files_pems_delete("system-id/some-dir")
+
+
+To edit permissions for another user, let's say her username is "collab,"
+to view a file:
+
+.. code-block:: pycon
+
+    ag.files_pems_update("system-id/path", "collab", "ALL", recursive=True)
+
+Now, a user with username "collab" has permissions to access the all contents
+of the specified directory (hence the ``recursive=True`` option).
+
+Valid values for setting permission are ``READ``, ``WRITE``, ``EXECUTE``, 
+``READ_WRITE``, ``READ_EXECUTE``, ``WRITE_EXECUTE``, ``ALL``, and ``NONE``.
+This same action can be performed recursively on directories using ``recursive=True``.


Test patch:
diff --git a/tests/files-pems_test.py b/tests/files-pems_test.py
index 068a513..0f3a662 100644
--- a/tests/files-pems_test.py
+++ b/tests/files-pems_test.py
@@ -46,6 +46,17 @@ class MockServerListingsEndpoints(BaseHTTPRequestHandler):
         self.send_response(200)
         self.end_headers()
 
+    def do_POST(self):
+        # Check that basic auth is used.
+        authorization = self.headers.get("Authorization")
+        if authorization == "" or authorization is None:
+            self.send_response(400)
+            self.end_headers()
+            return
+
+        self.send_response(200)
+        self.end_headers()
+
 
 
 class TestMockServer(MockServer):
@@ -81,7 +92,7 @@ class TestMockServer(MockServer):
         assert "200" in err 
 
 
-    def test_files_pems_list(self):
+    def test_files_pems_delete(self, capfd):
         """ Test permissions deletion
         """
         local_uri = "http://localhost:{port}/".format(port=self.mock_server_port)
@@ -92,3 +103,20 @@ class TestMockServer(MockServer):
 
         # Delete permissions.
         agave.files_pems_delete("tacc-globalfs-username/")
+        out, err = capfd.readouterr()
+        assert "200" in err
+
+
+    def test_files_pems_update(self, capfd):
+        """ Test permissions updates
+        """
+        local_uri = "http://localhost:{port}/".format(port=self.mock_server_port)
+        agave = Agave(api_server=local_uri)
+        agave.token = "mock-access-token"
+        agave.created_at = str(int(time.time()))
+        agave.expires_in = str(14400)
+
+        # Delete permissions.
+        agave.files_pems_update("tacc-globalfs-username/", "collaborator", "ALL")
+        out, err = capfd.readouterr()
+        assert "200" in err