diff --git a/opentech/apply/users/management/__init__.py b/opentech/apply/users/management/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/opentech/apply/users/management/commands/__init__.py b/opentech/apply/users/management/commands/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/opentech/apply/users/management/commands/migrate_users.py b/opentech/apply/users/management/commands/migrate_users.py
new file mode 100644
index 0000000000000000000000000000000000000000..f77444311de91d7fe1fbd37862104ffbccdda83d
--- /dev/null
+++ b/opentech/apply/users/management/commands/migrate_users.py
@@ -0,0 +1,80 @@
+import argparse
+import json
+
+from django.conf import settings
+from django.contrib.auth import get_user_model
+from django.contrib.auth.models import Group
+from django.core.management.base import BaseCommand
+from django.db import transaction
+
+from opentech.apply.users.groups import STAFF_GROUP_NAME
+
+
+class Command(BaseCommand):
+    help = "User migration script. Requires a source JSON file."
+    groups = Group.objects.all()
+
+    def add_arguments(self, parser):
+        parser.add_argument('source', type=argparse.FileType('r'), help='Migration source JSON file')
+
+    @transaction.atomic
+    def handle(self, *args, **options):
+        with options['source'] as json_data:
+            User = get_user_model()
+            users = json.load(json_data)
+
+            for uid in users:
+                user = users[uid]
+
+                full_name = self.get_full_name(user)
+                user_object, created = User.objects.get_or_create(
+                    email=user['mail'],
+                    defaults={
+                        'full_name': full_name,
+                        'drupal_id': uid,
+                    }
+                )
+
+                operation = "Imported" if created else "Processed"
+
+                groups = self.get_user_groups(user)
+                user_object.groups.set(groups)
+
+                # Ensure uid is set
+                user_object.drupal_id = uid
+                user_object.save()
+
+                self.stdout.write(f"{operation} user {uid} ({full_name})")
+
+    def get_full_name(self, user):
+        full_name = user.get('field_otf_real_name', None)
+        try:
+            # The Drupal data structure includes a language reference.
+            # The default is 'und' (undefined).
+            full_name = full_name['und'][0]['safe_value']
+        except (KeyError, TypeError):
+            full_name = user['name']
+
+        return full_name
+
+    def get_user_groups(self, user):
+        groups = []
+        role_map = {
+            'proposer': 'Applicant',
+            'council': 'Advisor',
+            'administrator': 'Administrator',
+            'dev': 'Administrator',
+        }
+
+        _, email_domain = user.get('mail').split('@')
+        if email_domain in settings.STAFF_EMAIL_DOMAINS:
+            groups.append(self.groups.filter(name=STAFF_GROUP_NAME).first())
+
+        roles = [role for role in user.get('roles').values() if role != "authenticated user"]
+
+        for role in roles:
+            group_name = role_map.get(role)
+            if group_name:
+                groups.append(self.groups.filter(name=group_name).first())
+
+        return groups
diff --git a/opentech/apply/users/migrations/0005_user_drupal_id.py b/opentech/apply/users/migrations/0005_user_drupal_id.py
new file mode 100644
index 0000000000000000000000000000000000000000..662c55217d96a415dcca24a8a5c7564c788a9534
--- /dev/null
+++ b/opentech/apply/users/migrations/0005_user_drupal_id.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.8 on 2018-02-28 15:21
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('users', '0004_drop_first_last_names'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='user',
+            name='drupal_id',
+            field=models.IntegerField(blank=True, editable=False, null=True),
+        ),
+    ]
diff --git a/opentech/apply/users/models.py b/opentech/apply/users/models.py
index 9e867ab9b60c9d430a1e0f79825514b20d043249..f4d22666087e3159ec7d4266bb9513d6b4f591ef 100644
--- a/opentech/apply/users/models.py
+++ b/opentech/apply/users/models.py
@@ -48,6 +48,9 @@ class User(AbstractUser):
     email = models.EmailField(_('email address'), unique=True)
     full_name = models.CharField(verbose_name='Full name', max_length=255, blank=True)
 
+    # Meta: used for migration purposes only
+    drupal_id = models.IntegerField(null=True, blank=True, editable=False)
+
     USERNAME_FIELD = 'email'
     REQUIRED_FIELDS = []