diff --git a/api/auth/__init__.py b/api/auth/__init__.py
index 44568af84ae4e70442a0cd95052ff9b6697d2810..ec2c9451fbec767056345fd60d6165f26e75cd05 100644
--- a/api/auth/__init__.py
+++ b/api/auth/__init__.py
@@ -22,6 +22,9 @@ def _get_access(uid, site, container):
             return INTEGER_ROLES[perm['access']]
     return -1
 
+def has_access(uid, container, perm, site='local'):
+    return _get_access(uid, site, container) >= INTEGER_ROLES[perm]
+
 def always_ok(exec_op):
     """
     This decorator leaves the original method unchanged.
diff --git a/api/base.py b/api/base.py
index 147ae7202c0dacb2449c6afa757630abb5e43c1b..6e2fd2adcf17efd41fac4edfa8a9450e4bbb20c5 100644
--- a/api/base.py
+++ b/api/base.py
@@ -13,7 +13,7 @@ from . import files
 from . import config
 from .types import Origin
 from . import validators
-from .dao import APIConsistencyException, APIConflictException, APINotFoundException
+from .dao import APIConsistencyException, APIConflictException, APINotFoundException, APIPermissionException
 
 
 class RequestHandler(webapp2.RequestHandler):
@@ -272,6 +272,8 @@ class RequestHandler(webapp2.RequestHandler):
             self.request.logger.warning(str(exception))
         elif isinstance(exception, APIConsistencyException):
             code = 400
+        elif isinstance(exception, APIPermissionException):
+            code = 403
         elif isinstance(exception, APINotFoundException):
             code = 404
         elif isinstance(exception, APIConflictException):
diff --git a/api/dao/__init__.py b/api/dao/__init__.py
index c788106ab9d2057ac2ddb30485c8ac110c5e08b5..1cbbcd8ec055a247397f96491ff565000f9acc8e 100644
--- a/api/dao/__init__.py
+++ b/api/dao/__init__.py
@@ -10,5 +10,8 @@ class APIConflictException(Exception):
 class APINotFoundException(Exception):
     pass
 
+class APIPermissionException(Exception):
+    pass
+
 def noop(*args, **kwargs): # pylint: disable=unused-argument
     pass
diff --git a/api/dao/hierarchy.py b/api/dao/hierarchy.py
index 63da5d75cd17cf178bceea06c93f78061f29ac75..a13eb93d8bde776951faba9d052edd337a11e0ee 100644
--- a/api/dao/hierarchy.py
+++ b/api/dao/hierarchy.py
@@ -9,7 +9,8 @@ import re
 from .. import files
 from .. import util
 from .. import config
-from . import APIStorageException, APINotFoundException, containerutil
+from ..auth import has_access
+from . import APIStorageException, APINotFoundException, APIPermissionException, containerutil
 
 log = config.log
 
@@ -161,24 +162,33 @@ def _group_id_fuzzy_match(group_id, project_label):
         group_id = 'unknown'
     return group_id, project_label
 
-def _find_or_create_destination_project(group_id, project_label, timestamp):
+def _find_or_create_destination_project(group_id, project_label, timestamp, user):
     group_id, project_label = _group_id_fuzzy_match(group_id, project_label)
     group = config.db.groups.find_one({'_id': group_id})
-    project = config.db.projects.find_one_and_update(
-        {'group': group['_id'],
-         'label': {'$regex': re.escape(project_label), '$options': 'i'}
-        },
-        {
-            '$setOnInsert': {
+
+    project = config.db.projects.find_one({'group': group['_id'],'label': {'$regex': re.escape(project_label), '$options': 'i'}})
+
+    if project:
+        # If the project already exists, check the user's access
+        if user and not has_access(user, project, 'rw'):
+            raise APIPermissionException('User {} does not have read-write access to project {}'.format(user, project['label']))
+        return project
+
+    else:
+        # if the project doesn't exit, check the user's access at the group level
+        if user and not has_access(user, group, 'rw'):
+            raise APIPermissionException('User {} does not have read-write access to group {}'.format(user, group_id))
+
+        project = {
+                'group': group['_id'],
                 'label': project_label,
-                'permissions': group['roles'], 'public': False,
-                'created': timestamp, 'modified': timestamp
-            }
-        },
-        PROJECTION_FIELDS,
-        upsert=True,
-        return_document=pymongo.collection.ReturnDocument.AFTER,
-        )
+                'permissions': group['roles'],
+                'public': False,
+                'created': timestamp,
+                'modified': timestamp
+        }
+        result = config.db.projects.insert_one(project)
+        project['_id'] = result.inserted_id
     return project
 
 def _create_query(cont, cont_type, parent_type, parent_id, upload_type):
@@ -194,7 +204,7 @@ def _create_query(cont, cont_type, parent_type, parent_id, upload_type):
             'uid': cont['uid']
         }
     else:
-        raise NotImplementedError('upload type is not handled by _create_query')
+        raise NotImplementedError('upload type {} is not handled by _create_query'.format(upload_type))
 
 def _upsert_container(cont, cont_type, parent, parent_type, upload_type, timestamp):
     cont['modified'] = timestamp
@@ -261,7 +271,7 @@ def _get_targets(project_obj, session, acquisition, type_, timestamp):
     return target_containers
 
 
-def find_existing_hierarchy(metadata):
+def find_existing_hierarchy(metadata, user=None):
     project = metadata.get('project', {})
     session = metadata.get('session', {})
     acquisition = metadata.get('acquisition', {})
@@ -276,8 +286,12 @@ def find_existing_hierarchy(metadata):
 
     # Confirm session and acquisition exist
     session_obj = config.db.sessions.find_one({'uid': session_uid}, ['project'])
+
     if session_obj is None:
         raise APINotFoundException('Session with uid {} does not exist'.format(session_uid))
+    if user and not has_access(user, session_obj, 'rw'):
+        raise APIPermissionException('User {} does not have read-write access to session {}'.format(user, session_uid))
+
     a = config.db.acquisitions.find_one({'uid': acquisition_uid}, ['_id'])
     if a is None:
         raise APINotFoundException('Acquisition with uid {} does not exist'.format(acquisition_uid))
@@ -292,7 +306,7 @@ def find_existing_hierarchy(metadata):
     return target_containers
 
 
-def upsert_bottom_up_hierarchy(metadata):
+def upsert_bottom_up_hierarchy(metadata, user=None):
     group = metadata.get('group', {})
     project = metadata.get('project', {})
     session = metadata.get('session', {})
@@ -310,6 +324,10 @@ def upsert_bottom_up_hierarchy(metadata):
 
     session_obj = config.db.sessions.find_one({'uid': session_uid}, ['project'])
     if session_obj: # skip project creation, if session exists
+
+        if user and not has_access(user, session_obj, 'rw'):
+            raise APIPermissionException('User {} does not have read-write access to session {}'.format(user, session_uid))
+
         now = datetime.datetime.utcnow()
         project_files = dict_fileinfos(project.pop('files', []))
         project_obj = config.db.projects.find_one({'_id': session_obj['project']}, projection=PROJECTION_FIELDS + ['name'])
@@ -319,10 +337,11 @@ def upsert_bottom_up_hierarchy(metadata):
         )
         return target_containers
     else:
-        return upsert_top_down_hierarchy(metadata, 'uid')
+        return upsert_top_down_hierarchy(metadata, 'uid', user=user)
 
 
-def upsert_top_down_hierarchy(metadata, type_='label'):
+def upsert_top_down_hierarchy(metadata, type_='label', user=None):
+    log.debug('I know my type is {}'.format(type_))
     group = metadata['group']
     project = metadata['project']
     session = metadata.get('session')
@@ -330,7 +349,7 @@ def upsert_top_down_hierarchy(metadata, type_='label'):
 
     now = datetime.datetime.utcnow()
     project_files = dict_fileinfos(project.pop('files', []))
-    project_obj = _find_or_create_destination_project(group['_id'], project['label'], now)
+    project_obj = _find_or_create_destination_project(group['_id'], project['label'], now, user)
     target_containers = _get_targets(project_obj, session, acquisition, type_, now)
     target_containers.append(
         (TargetContainer(project_obj, 'project'), project_files)
diff --git a/api/placer.py b/api/placer.py
index 8796f8fa9b17a8379c1d96f58832e94367cc0e61..2298558539fe495d1bd3e04fba0dfd00fa801482 100644
--- a/api/placer.py
+++ b/api/placer.py
@@ -138,7 +138,8 @@ class UIDPlacer(Placer):
         metadata_validator = validators.from_schema_path(payload_schema_uri)
         metadata_validator(self.metadata, 'POST')
 
-        targets = self.create_hierarchy(self.metadata)
+        # If not a superuser request, pass uid of user making the upload request
+        targets = self.create_hierarchy(self.metadata, user=self.context.get('uid'))
 
         self.metadata_for_file = {}
 
diff --git a/api/upload.py b/api/upload.py
index c3fa30238ac5694b1030cb2f3a45e8e8e08c9b04..65c748fe58245f40d1ee6cbda82bc5934ae12fb0 100644
--- a/api/upload.py
+++ b/api/upload.py
@@ -143,7 +143,11 @@ class Upload(base.RequestHandler):
         """Receive a sortable reaper upload."""
 
         if not self.superuser_request:
-            self.abort(402, 'uploads must be from an authorized drone')
+            user = self.uid
+            if not user:
+                self.abort(403, 'Uploading requires login')
+
+        context = {'uid': self.uid if not self.superuser_request else None}
 
         # TODO: what enum
         if strategy == 'label':
@@ -154,7 +158,7 @@ class Upload(base.RequestHandler):
             strategy = Strategy.uidmatch
         else:
             self.abort(500, 'stragegy {} not implemented'.format(strategy))
-        return process_upload(self.request, strategy, origin=self.origin)
+        return process_upload(self.request, strategy, origin=self.origin, context=context)
 
     def engine(self):
         """Handles file uploads from the engine"""