diff --git a/nomad/app/api/upload.py b/nomad/app/api/upload.py
index 6282cdc6db22a724b5924bfbbd1bfa92b7ba418b..5682aa113fa2af7113a54377ac1cc2cf21969baf 100644
--- a/nomad/app/api/upload.py
+++ b/nomad/app/api/upload.py
@@ -122,8 +122,10 @@ upload_metadata_parser = api.parser()
 upload_metadata_parser.add_argument('name', type=str, help='An optional name for the upload.', location='args')
 upload_metadata_parser.add_argument('local_path', type=str, help='Use a local file on the server.', location='args')
 upload_metadata_parser.add_argument('token', type=str, help='Upload token to authenticate with curl command.', location='args')
+upload_metadata_parser.add_argument('oasis_upload_id', type=str, help='Use if this is an upload from an OASIS to the central NOMAD and set it to the upload_id.', location='args')
 upload_metadata_parser.add_argument('file', type=FileStorage, help='The file to upload.', location='files')
 
+
 upload_list_parser = pagination_request_parser.copy()
 upload_list_parser.add_argument('state', type=str, help='List uploads with given state: all, unpublished, published.', location='args')
 upload_list_parser.add_argument('name', type=str, help='Filter for uploads with the given name.', location='args')
@@ -249,8 +251,23 @@ class UploadListResource(Resource):
             if Upload.user_uploads(g.user, published=False).count() >= config.services.upload_limit:
                 abort(400, 'Limit of unpublished uploads exceeded for user.')
 
+        # check if allowed to perform oasis upload
+        oasis_upload_id = request.args.get('oasis_upload_id')
+        from_oasis = oasis_upload_id is not None
+        if from_oasis is not None:
+            if not g.user.is_oasis_admin:
+                abort(401, 'Only an oasis admin can perform an oasis upload.')
+
         upload_name = request.args.get('name')
-        upload_id = utils.create_uuid()
+        if oasis_upload_id is not None:
+            upload_id = oasis_upload_id
+            try:
+                Upload.get(upload_id)
+                abort(400, 'An oasis upload with the given upload_id already exists.')
+            except KeyError:
+                pass
+        else:
+            upload_id = utils.create_uuid()
 
         logger = common.logger.bind(upload_id=upload_id, upload_name=upload_name)
         logger.info('upload created', )
@@ -310,7 +327,8 @@ class UploadListResource(Resource):
             name=upload_name,
             upload_time=datetime.utcnow(),
             upload_path=upload_path,
-            temporary=local_path != upload_path)
+            temporary=local_path != upload_path,
+            from_oasis=from_oasis)
 
         upload.process_upload()
         logger.info('initiated processing')
diff --git a/nomad/datamodel/datamodel.py b/nomad/datamodel/datamodel.py
index 21fa3cb541dc7d4ceaf9fdc17e16dcc5395cd961..71a316d1d448274052ad21637820073d40eaeb12 100644
--- a/nomad/datamodel/datamodel.py
+++ b/nomad/datamodel/datamodel.py
@@ -106,6 +106,8 @@ class User(Author):
     is_admin = metainfo.Quantity(
         type=bool, derived=lambda user: user.user_id == config.services.admin_user_id)
 
+    is_oasis_admin = metainfo.Quantity(type=bool, default=False)
+
     @staticmethod
     @cached(cache=TTLCache(maxsize=2048, ttl=24 * 3600))
     def get(*args, **kwargs) -> 'User':
diff --git a/nomad/infrastructure.py b/nomad/infrastructure.py
index 7509413a9f2c0593ed6f67d8a2f5c6d7438cf32c..2115e24140ab81b358e5ac86b43bf6c0199b267e 100644
--- a/nomad/infrastructure.py
+++ b/nomad/infrastructure.py
@@ -336,12 +336,14 @@ class Keycloak():
         from nomad import datamodel
 
         kwargs = {key: value[0] for key, value in keycloak_user.get('attributes', {}).items()}
+        oasis_admin = kwargs.pop('is_oasis_admin', None) is not None
         return datamodel.User(
             user_id=keycloak_user['id'],
             email=keycloak_user.get('email'),
             username=keycloak_user.get('username'),
             first_name=keycloak_user.get('firstName'),
             last_name=keycloak_user.get('lastName'),
+            is_oasis_admin=oasis_admin,
             created=datetime.fromtimestamp(keycloak_user['createdTimestamp'] / 1000),
             **kwargs)
 
diff --git a/nomad/processing/data.py b/nomad/processing/data.py
index df3eb63f27777203e182d92381b88a502834032d..46c6a1eada560bd1c41be16228c78778a17f7f44 100644
--- a/nomad/processing/data.py
+++ b/nomad/processing/data.py
@@ -1208,17 +1208,13 @@ class Upload(Proc):
                 self.upload_files.delete()
 
             if metadata is not None:
-                self.publish_time = metadata.get('publish_time')
                 self.upload_time = metadata.get('upload_time')
 
-            if self.publish_time is None:
-                self.publish_time = datetime.utcnow()
-                logger.warn('oasis upload without publish time')
-
             if self.upload_time is None:
                 self.upload_time = datetime.utcnow()
                 logger.warn('oasis upload without upload time')
 
+            self.publish_time = datetime.utcnow()
             self.published = True
             self.last_update = datetime.utcnow()
             self.save()
diff --git a/tests/app/test_api.py b/tests/app/test_api.py
index 0483280ec6f5624eac67eb88dc718cff448a4471..9c2727d8cdc9187e218de438a095989336282d96 100644
--- a/tests/app/test_api.py
+++ b/tests/app/test_api.py
@@ -171,11 +171,12 @@ class TestAuth:
         assert rv.status_code == 200
         data = json.loads(rv.data)
         assert len(data['users'])
-        keys = data['users'][0].keys()
+        user = data['users'][0]
+        keys = user.keys()
         required_keys = ['name', 'email', 'user_id']
         assert all(key in keys for key in required_keys)
         for key in keys:
-            assert data['users'][0].get(key) is not None
+            assert user.get(key) is not None
 
     def test_invite(self, api, test_user_auth, no_warn):
         rv = api.put(
@@ -530,6 +531,44 @@ class TestUploads:
         rv = api.get('/raw/%s/examples_potcar/POTCAR%s.stripped' % (upload_id, ending))
         assert rv.status_code == 200
 
+    def test_post_from_oasis_admin(self, api, other_test_user_auth, oasis_example_upload, proc_infra, no_warn):
+        rv = api.put(
+            '/uploads/?local_path=%s&oasis_upload_id=oasis_upload_id' % oasis_example_upload,
+            headers=other_test_user_auth)
+        assert rv.status_code == 401
+
+    def test_post_from_oasis_duplicate(self, api, test_user, test_user_auth, oasis_example_upload, proc_infra, no_warn):
+        Upload.create(upload_id='oasis_upload_id', user=test_user).save()
+        rv = api.put(
+            '/uploads/?local_path=%s&oasis_upload_id=oasis_upload_id' % oasis_example_upload,
+            headers=test_user_auth)
+        assert rv.status_code == 400
+
+    def test_post_from_oasis(self, api, test_user_auth, oasis_example_upload, proc_infra, no_warn):
+        rv = api.put(
+            '/uploads/?local_path=%s&oasis_upload_id=oasis_upload_id' % oasis_example_upload,
+            headers=test_user_auth)
+        assert rv.status_code == 200
+        upload = self.assert_upload(rv.data)
+        upload_id = upload['upload_id']
+        assert upload_id == 'oasis_upload_id'
+
+        # poll until completed
+        upload = self.block_until_completed(api, upload_id, test_user_auth)
+
+        assert len(upload['tasks']) == 4
+        assert upload['tasks_status'] == SUCCESS
+        assert upload['current_task'] == 'cleanup'
+        assert not upload['process_running']
+
+        upload_proc = Upload.objects(upload_id=upload_id).first()
+        assert upload_proc.published
+        assert upload_proc.from_oasis
+
+        entries = get_upload_entries_metadata(upload)
+        assert_upload_files(upload_id, entries, files.PublicUploadFiles)
+        assert_search_upload(entries, additional_keys=['atoms', 'dft.system'])
+
 
 today = datetime.datetime.utcnow().date()
 today_datetime = datetime.datetime(*today.timetuple()[:6])
diff --git a/tests/conftest.py b/tests/conftest.py
index d4ea0b531ef4ab8949177829ab2b2f1043fca433..382dbb19c8a2e9c6f83d6d1f1bb6d09d2e895b89 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -34,6 +34,7 @@ from typing import List
 import json
 import logging
 import warnings
+import zipfile
 
 from nomad import config, infrastructure, processing, app, utils
 from nomad.datamodel import EntryArchive
@@ -227,7 +228,7 @@ def test_user_uuid(handle):
 
 test_users = {
     test_user_uuid(0): dict(username='admin', email='admin', user_id=test_user_uuid(0)),
-    test_user_uuid(1): dict(username='scooper', email='sheldon.cooper@nomad-coe.eu', first_name='Sheldon', last_name='Cooper', user_id=test_user_uuid(1)),
+    test_user_uuid(1): dict(username='scooper', email='sheldon.cooper@nomad-coe.eu', first_name='Sheldon', last_name='Cooper', user_id=test_user_uuid(1), is_oasis_admin=True),
     test_user_uuid(2): dict(username='lhofstadter', email='leonard.hofstadter@nomad-coe.eu', first_name='Leonard', last_name='Hofstadter', user_id=test_user_uuid(2))
 }
 
@@ -266,7 +267,7 @@ class KeycloakMock:
     def search_user(self, query):
         return [
             User(**test_user) for test_user in self.users.values()
-            if query in ' '.join(test_user.values())]
+            if query in ' '.join([str(value) for value in test_user.values()])]
 
     @property
     def access_token(self):
@@ -576,6 +577,39 @@ def non_empty_uploaded(non_empty_example_upload: str, raw_files) -> Tuple[str, s
     return example_upload_id, non_empty_example_upload
 
 
+@pytest.fixture(scope='function')
+def oasis_example_upload(non_empty_example_upload: str, raw_files) -> str:
+    processing.Upload.metadata_file_cached.cache_clear()
+
+    uploaded_path = non_empty_example_upload
+    uploaded_path_modified = os.path.join(
+        config.fs.tmp,
+        os.path.basename(non_empty_example_upload))
+    shutil.copyfile(uploaded_path, uploaded_path_modified)
+
+    metadata = {
+        'upload_id': 'oasis_upload_id',
+        'upload_time': '2020-01-01 00:00:00',
+        'published': True,
+        'entries': {
+            'examples_template/template.json': {
+                'calc_id': 'test_calc_id'
+            }
+        }
+    }
+
+    with zipfile.ZipFile(uploaded_path_modified, 'a') as zf:
+        with zf.open('nomad.json', 'w') as f:
+            f.write(json.dumps(metadata).encode())
+
+    return uploaded_path_modified
+
+
+@pytest.fixture(scope='function')
+def oasis_example_uploaded(oasis_example_upload: str) -> Tuple[str, str]:
+    return 'oasis_upload_id', oasis_example_upload
+
+
 @pytest.mark.timeout(config.tests.default_timeout)
 @pytest.fixture(scope='function')
 def processed(uploaded: Tuple[str, str], test_user: User, proc_infra) -> processing.Upload:
diff --git a/tests/processing/test_data.py b/tests/processing/test_data.py
index 45aa0de5f43a9266b70c220d7be0b035eb898e6e..15d934483d04f9717f17a3f43ae3e7d1f6832b3a 100644
--- a/tests/processing/test_data.py
+++ b/tests/processing/test_data.py
@@ -22,7 +22,6 @@ from datetime import datetime
 import os.path
 import re
 import shutil
-import json
 
 from nomad import utils, infrastructure, config
 from nomad.archive import read_partial_archive_from_mongo
@@ -205,33 +204,12 @@ def test_publish_failed(
         assert_search_upload(entries, additional_keys, published=True, processed=False)
 
 
-@pytest.mark.timeout(5)
-def test_oasis_upload_processing(proc_infra, non_empty_uploaded: Tuple[str, str], test_user):
-    Upload.metadata_file_cached.cache_clear()
-    from shutil import copyfile
-    import zipfile
-
-    uploaded_id, uploaded_path = non_empty_uploaded
-    uploaded_zipfile = os.path.join(config.fs.tmp, 'upload.zip')
-    copyfile(uploaded_path, uploaded_zipfile)
-
-    metadata = {
-        'upload_id': uploaded_id,
-        'upload_time': '2020-01-01 00:00:00',
-        'published': True,
-        'entries': {
-            'examples_template/template.json': {
-                'calc_id': 'test_calc_id'
-            }
-        }
-    }
-
-    with zipfile.ZipFile(uploaded_zipfile, 'a') as zf:
-        with zf.open('nomad.json', 'w') as f:
-            f.write(json.dumps(metadata).encode())
+@pytest.mark.timeout(config.tests.default_timeout)
+def test_oasis_upload_processing(proc_infra, oasis_example_uploaded: Tuple[str, str], test_user, no_warn):
+    uploaded_id, uploaded_path = oasis_example_uploaded
 
     upload = Upload.create(
-        upload_id=uploaded_id, user=test_user, upload_path=uploaded_zipfile)
+        upload_id=uploaded_id, user=test_user, upload_path=uploaded_path)
     upload.from_oasis = True
 
     assert upload.tasks_status == 'RUNNING'
@@ -241,8 +219,11 @@ def test_oasis_upload_processing(proc_infra, non_empty_uploaded: Tuple[str, str]
     upload.block_until_complete(interval=.01)
 
     assert upload.published
-    assert str(upload.upload_time) == metadata['upload_time']
+    assert str(upload.upload_time) == '2020-01-01 00:00:00'
     assert_processing(upload, published=True)
+    calc = Calc.objects(upload_id='oasis_upload_id').first()
+    assert calc.calc_id == 'test_calc_id'
+    assert calc.metadata['published']
 
 
 @pytest.mark.timeout(config.tests.default_timeout)