Repository: girder/girder
Issue: Cannot delete NEX-DCP30 S3 assetstore

Trying to delete an asset store with the following values:

```
Type: S3
Bucket: nasanex
Path prefix: NEX-DCP30
Access key ID:
Secret access key:
Service:
```

I recieve a 403 error with the following response

```
message: "S3ResponseError: S3ResponseError: 403 Forbidden↵<?xml version="1.0" encoding="UTF-8"?>↵<Error><Code>AccessDenied</Code><Message>Access Denied</Message><RequestId>F7620A9CA788F786</RequestId><HostId>Ek3ChCUDpiF9Nyko8YVgdsmhJgpbMxJE62e914dqoauzVhqa7gtqQCT5LoluxWJo5Hd1PCLWX/4=</HostId></Error>"
trace: [["girder/api/rest.py", 295, "endpointDecorator", "val = fun(self, args, kwargs)"],…]
0: ["girder/api/rest.py", 295, "endpointDecorator", "val = fun(self, args, kwargs)"]
1: ["girder/api/rest.py", 777, "DELETE", "return self.handleRoute('DELETE', path, params)"]
2: ["girder/api/rest.py", 583, "handleRoute", "val = handler(**kwargs)"]
3: ["girder/api/access.py", 34, "accessDecorator", "return fun(*args, **kwargs)"]
4: ["girder/api/rest.py", 243, "wrapped", "return fun(*args, **kwargs)"]
5: ["girder/api/v1/assetstore.py", 244, "deleteAssetstore", "self.model('assetstore').remove(assetstore)"]
6: ["girder/models/assetstore.py", 82, "remove", "adapter.untrackedUploads([], 'delete')"]
7: ["girder/utility/s3_assetstore_adapter.py", 478, "untrackedUploads", "multipartUpload.cancel_upload()"]
8: ["/home/kotfic/.venvs/NEX/lib/python2.7/site-packages/boto/s3/multipart.py", 330, "cancel_upload",…]
9: ["/home/kotfic/.venvs/NEX/lib/python2.7/site-packages/boto/s3/bucket.py", 1820,…]
type: "internal"
```

I am logged in as the admin user. All items have been removed that refer to the asset store and the delete button is 'clickable.' 


Changed files:
girder/models/assetstore.py, tests/cases/assetstore_test.py, girder/utility/s3_assetstore_adapter.py

diff --git a/girder/models/assetstore.py b/girder/models/assetstore.py
index b8b81c9c6..fbceae21e 100644
--- a/girder/models/assetstore.py
+++ b/girder/models/assetstore.py
@@ -79,7 +79,7 @@ class Assetstore(Model):
         # delete partial uploads before we delete the store.
         adapter = assetstore_utilities.getAssetstoreAdapter(assetstore)
         try:
-            adapter.untrackedUploads([], 'delete')
+            adapter.untrackedUploads([], delete=True)
         except ValidationException:
             # this assetstore is currently unreachable, so skip this step
             pass
diff --git a/girder/utility/s3_assetstore_adapter.py b/girder/utility/s3_assetstore_adapter.py
index 525642b2d..566fbba61 100644
--- a/girder/utility/s3_assetstore_adapter.py
+++ b/girder/utility/s3_assetstore_adapter.py
@@ -435,7 +435,7 @@ class S3AssetstoreAdapter(AbstractAssetstoreAdapter):
                     getParams['upload_id_marker'] = \\
                         multipartUploads.next_upload_id_marker
 
-    def untrackedUploads(self, knownUploads=[], delete=False):
+    def untrackedUploads(self, knownUploads=None, delete=False):
         """
         List and optionally discard uploads that are in the assetstore but not
         in the known list.
@@ -446,11 +446,17 @@ class S3AssetstoreAdapter(AbstractAssetstoreAdapter):
         :type delete: bool
         :returns: a list of unknown uploads.
         """
+        if self.assetstore.get('readOnly'):
+            return []
+
         untrackedList = []
         prefix = self.assetstore.get('prefix', '')
         if prefix:
             prefix += '/'
 
+        if knownUploads is None:
+            knownUploads = []
+
         bucket = self._getBucket()
         if not bucket:
             return []
diff --git a/tests/cases/assetstore_test.py b/tests/cases/assetstore_test.py
index 05080eaad..1f50e1c2f 100755
--- a/tests/cases/assetstore_test.py
+++ b/tests/cases/assetstore_test.py
@@ -24,6 +24,7 @@ import mock
 import moto
 import os
 import six
+import sys
 import time
 import zipfile
 
@@ -582,3 +583,25 @@ class AssetstoreTestCase(base.TestCase):
                 break  # give up and fail
             time.sleep(0.1)
         self.assertIsNone(bucket.get_key(file['s3Key']))
+
+        resp = self.request(path='/folder/%s' % parentFolder['_id'],
+                            method='DELETE', user=self.admin)
+        self.assertStatusOk(resp)
+
+        # Set the assetstore to read only, attempt to delete it
+        assetstore['readOnly'] = True
+        self.model('assetstore').save(assetstore)
+
+        def fn(*args, **kwargs):
+            raise Exception('get_all_multipart_uploads should not be called')
+
+        # Must mock globally (too tricky to get a direct mock.patch)
+        old = sys.modules['boto.s3.bucket'].Bucket.get_all_multipart_uploads
+        sys.modules['boto.s3.bucket'].Bucket.get_all_multipart_uploads = fn
+
+        try:
+            resp = self.request(path='/assetstore/%s' % assetstore['_id'],
+                                method='DELETE', user=self.admin)
+            self.assertStatusOk(resp)
+        finally:
+            sys.modules['boto.s3.bucket'].Bucket.get_all_multipart_uploads = old